David David - 2 months ago 26
Swift Question

Strange UIView behaviour when changing Y origin when Keyboard is visible

Context:



I have a UIView containing a UITextView. I want to move that UIView up when the keyboard is visible. So the idea is to change the Y position.

Problem:



Sometimes, the UIView goes back to its initial Y position while the keyboard is still visible. And it doesn't come back anymore even if I focus the input. I really don't know why.

Illustration (gif):



https://d17oy1vhnax1f7.cloudfront.net/items/2K0z3P1j1x2f0X2X1B2v/ci.gif

PS: The keyboard appears 3 times in the Gif. It is looping so it is hard to distinguish the end.

Code



What I did, first I've added notification observer:

NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillChangeFrame), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)


Then I've defined the keyboard toggle handler :

func keyboardWillChangeFrame(notification: NSNotification) {

let info = notification.userInfo!
let keyboardFrame: CGRect = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue

self.keyboardInfo["isVisible"] = self.view.frame.size.height - keyboardFrame.origin.y != 0
self.keyboardInfo["height"] = keyboardFrame.size.height
self.keyboardInfo["animationDuration"] = info[UIKeyboardAnimationDurationUserInfoKey] as! Double
self.keyboardInfo["animationCurve"] = info[UIKeyboardAnimationCurveUserInfoKey] as! UInt

if self.keyboardInfo["isVisible"] as! Bool {
self.moveCommentInputUp()
} else {
self.moveCommentInputDown()
}

}


After that, I've defined the methods which move up/down the comment input view:

func moveCommentInputUp() {

print("Moving up... ", self.commentFormView.frame.origin.y, " to ", self.view.frame.size.height - (self.keyboardInfo["height"] as! CGFloat) - self.commentFormView.frame.size.height)

UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(self.keyboardInfo["animationDuration"] as! TimeInterval)
UIView.setAnimationCurve(UIViewAnimationCurve(rawValue: Int(self.keyboardInfo["animationCurve"] as! UInt))!)
UIView.setAnimationBeginsFromCurrentState(true)

ViewUtil.changeViewFrame(view: self.commentFormView, yPosition: self.view.frame.size.height - (self.keyboardInfo["height"] as! CGFloat) - self.commentFormView.frame.size.height)
ViewUtil.removeShadowToView(self.commentFormLauncherButton)

UIView.commitAnimations()

}

func moveCommentInputDown() {

print("Moving down... ", self.commentFormView.frame.origin.y, " to ", self.view.frame.size.height)

UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(self.keyboardInfo["animationDuration"] as! TimeInterval)
UIView.setAnimationCurve(UIViewAnimationCurve(rawValue: Int(self.keyboardInfo["animationCurve"] as! UInt))!)
UIView.setAnimationBeginsFromCurrentState(true)

ViewUtil.changeViewFrame(view: self.commentFormView, yPosition: self.view.frame.size.height)
ViewUtil.addShadowToView(commentFormLauncherButton, position: CGSize(width: 0, height: 5))

UIView.commitAnimations()

}


The action of the button is

@IBAction func showCommentForm(_ sender: UIButton) {

self.commentInput.becomeFirstResponder()

}


Logs (please refer to the Gif) are showing that the view moves from 568 - bottom of the screen - to 568 again

// The numbers show the initial and the final value of the keyboard Y position

// Click on button to focus the input
Moving up... 568.0 to 282.0
// Tap anywhere
Moving down... 282.0 to 568.0

// Click again on button to focus the input
Moving up... 568.0 to 282.0
///// Write "hhh". The view is gone, I don't know why.
// Tap anywhere
Moving down... 568.0 to 568.0 // The view seems to move from 568 to 568, hugh

// Click on button to focus the input
Moving up... 568.0 to 282.0 // This didn't work apparently
// Tap anywhere
Moving down... 568.0 to 568.0 // 568 to 568 again


Did I miss something?

System: iOS 9, Xcode 8, Swift 3

Answer

Okay so this is a solution which doesn't require CocoaPods.

My approach involves setting the constant of the textField's bottom constraint when the keyboard appears and disappears. This means you need to create a reference to the textField's bottom constraint.

@IBOutlet weak var textFieldBottomConstraint: NSLayoutConstraint!

I started with setting the observers on NSNotification.Name.UIKeyboardWillShow and UIKeyboardWillHide in viewDidLoad, instead of UIKeyboardWillChangeFrame:

NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: NSNotification.Name.UIKeyboardWillHide, object: nil)

Next up I set the selector and its function to set the constant of the textField's bottom constraint:

func handleKeyboardNotification(notification: NSNotification) {
    if let userInfo = notification.userInfo {
        let keyboardFrameValue = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)
        let keyboardFrame = keyboardFrameValue?.cgRectValue

        let isKeyboardShowing = notification.name == NSNotification.Name.UIKeyboardWillShow

        textFieldBottomConstraint.constant = isKeyboardShowing ? keyboardFrame!.height + 4 : 4
        self.view.layoutIfNeeded()
    }
}

Note that textFieldBottomConstraint is the reference variable to the bottom constraint.

What this does is makes textField set it's bottom constraint whenever the keyboard appears or disappears, and doesn't suffer from the weird glitches that you are experiencing.

Hope this helps and don't hesitate to ask for clarification!

Credits to YouTuber Let's Build that App! for providing the solution through this video:

https://www.youtube.com/watch?v=p8IaS5lmhuM