Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

keyboardWillShow gets called for other app's keyboards

I know this is what's supposed to happen, but it's causing me problems that I don't know how to fix.

I want to move my view up when the keyboard shows, so that my text fields remain visible.

My text fields have numeric keypads.

I use notifications and keyboardWillShow/Hide to move my view up/down when a text field is selected.

Now suppose I tap on a text field and then switch to another app that's using a different keyboard (not the numeric keypad). keyboardWillShow is called with the size of the wrong keyboard (the one from the other app) and my view is moved the wrong amount (it shouldn't even move at all). So when I go back to my app my view is at the wrong place and the keyboard isn't even showing, and then keyboardWillHide gets called and the view is moved back into place (out of nowhere). But keyboardWillShow shouldn't even be called for the other app in the first place.

I'm removing the notifications on viewWillDisappear, but this still happens… maybe keyboardWillShow is called for the other apps before viewWillDisappear is called for mine?

Here's my code:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
    for subview in self.view.subviews {
        if subview.isKindOfClass(UITextField) {
            let textField = subview as! UITextField
            textField.addTarget(self, action: "textFieldDidReturn:", forControlEvents: UIControlEvents.EditingDidEndOnExit)
            textField.addTarget(self, action: "textFieldDidBeginEditing:", forControlEvents: UIControlEvents.EditingDidBegin)
        }
    }
}

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    NSNotificationCenter.defaultCenter().removeObserver(self)
}

func keyboardWillShow(notification: NSNotification) {
    self.keyboardIsShowing = true
    if let info = notification.userInfo {
       self.keyboardFrame = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
       self.arrangeViewOffsetFromKeyboard()
    }
}

func keyboardWillHide(notification: NSNotification) {
    self.keyboardIsShowing = false
    self.returnViewToInitialFrame()
}

func arrangeViewOffsetFromKeyboard() {
    if let textField = activeTextField {
        let theApp: UIApplication = UIApplication.sharedApplication()
        let windowView: UIView? = theApp.delegate!.window!
        let textFieldLowerPoint = CGPoint(x: textField.frame.origin.x, y: textField.frame.origin.y + textField.frame.size.height)
        let convertedTextFieldLowerPoint = textField.superview!.convertPoint(textFieldLowerPoint, toView: windowView)
        let targetTextFieldLowerPoint = CGPoint(x: textField.frame.origin.x, y: self.keyboardFrame.origin.y)
        let targetPointOffset = targetTextFieldLowerPoint.y - convertedTextFieldLowerPoint.y
        let adjustedViewFrameCenter = CGPoint(x: self.view.center.x, y: self.view.center.y + targetPointOffset)
        print(targetPointOffset) // When I change to a different app this prints the wrong value… but none of this should even get called.
        if targetPointOffset < 0 {
            UIView.animateWithDuration(0.3, animations: {
                self.view.center = adjustedViewFrameCenter
            })
        }
    }
}

func returnViewToInitialFrame() {
    let initialViewRect = CGRect(x: 0.0, y: 0.0, width: self.view.frame.size.width, height: self.view.frame.size.height)
    if !CGRectEqualToRect(initialViewRect, self.view.frame) {
        UIView.animateWithDuration(0.2, animations: {
            self.view.frame = initialViewRect
        })
    }
}

Edit: As @JasonNam pointed out in his answer, viewWillDisappear doesn't get called when switching apps, so I had to add an applicationWillResignActive notification to remove the keyboard notifications and an applicationDidBecomeActive notification to add them back.


Edit 2: @sahara108's solution seems cleaner and I can't see any drawbacks. I just had to check for UIApplication.sharedApplication().applicationState == .Active before doing anything in keyboardWillShow.

like image 567
dbmrq Avatar asked Dec 22 '15 06:12

dbmrq


2 Answers

I suggest you to check if your textField is first responder in keyboardWillShown method. If it is not, just ignore the notification.

func keyboardWillShow(notification: NSNotification) {
    if !myTextField.isFirstResponder() {
        return
    }
    self.keyboardIsShowing = true
    if let info = notification.userInfo {
       self.keyboardFrame = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
       self.arrangeViewOffsetFromKeyboard()
    }
}

UPDATE: Instead of checking for the firstResponder, it is safer if you check UIApplication.shareApplication().applicationSate == .Active

like image 69
sahara108 Avatar answered Sep 21 '22 06:09

sahara108


iOS 9+ only:

NSNotification that comes from keyboard contains following:

UIKeyboardIsLocalUserInfoKey - The key for an NSNumber object containing a Boolean that identifies whether the keyboard belongs to the current app.

In my case i also do this (which is probably needed for OP too):

func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
    return UIApplication.shared.applicationState == .active
}

This way keyboard won't hide when switching between applications.

like image 36
Oleksii Nezhyborets Avatar answered Sep 20 '22 06:09

Oleksii Nezhyborets