7

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.

dbmrq
  • 1,451
  • 13
  • 32
  • One big problem in your code. You add `self` as the notification observer but you try to remove `self.view`. You need to remove `self`. – rmaddy Dec 22 '15 at 06:25
  • Oops, I was originally removing `self`, but I noticed this problem and started trying all kinds of crazy things, including removing the observer from `self.view` instead of `self`, then I forgot to change that part back when I posted the code here. The problem remains either way, though. And looking around I found out that apparently this is supposed to happen… but I don't want it to, and I don't know what to do. – dbmrq Dec 22 '15 at 06:33

7 Answers7

10

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

sahara108
  • 2,829
  • 1
  • 22
  • 41
9

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.

2

Just check whether the app state is active will be fine:

- (void)handleKeyboardWillShowNotification:(NSNotification *)notifaction{
    if([UIApplication sharedApplication].applicationState != UIApplicationStateActive){
        return;
    }
    //your code below...
}
蒋业文
  • 31
  • 1
0

You thought almost right right: You have to remove the specific notifications in viewWillDisappear:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)

    let notificationCenter = NSNotificationCenter.defaultCenter()
    notificationCenter.removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
    notificationCenter.removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}
Jörn Buitink
  • 2,906
  • 2
  • 22
  • 33
  • This isn't true. `notificationCenter.removeObserver(self)` will remove `self` from all registered notifications. The problem with the OP's code is that they attempt to remove `self.view` instead of `self`. – rmaddy Dec 22 '15 at 06:27
  • yep, might work, but isn't best practice: _This is safe to do in the dealloc method, but should not otherwise be used—use removeObserver:name:object: instead._ [link](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSNotificationCenter_Class/#//apple_ref/occ/instm/NSNotificationCenter/removeObserver:) – Jörn Buitink Dec 22 '15 at 06:30
  • @jorn Just tried that, it didn't solve my problem. :/ – dbmrq Dec 22 '15 at 06:36
0

You can deregister the notification on UIApplicationDidEnterBackgroundNotification and register again in UIApplicationDidBecomeActiveNotification. I cannot sure that that kind of behaviour is intentional but definitely something unexpected for me too.

override func viewDidLoad() {
    super.viewDidLoad()

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "applicationBecomeActive", name: UIApplicationDidBecomeActiveNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "applicationDidEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)
}

func applicationBecomeActive()
{
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
}

func applicationDidEnterBackground()
{
    NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}
Jason Nam
  • 2,011
  • 2
  • 13
  • 22
  • Hey would you check my answer again. Actually viewDidDisappear doesn't be called when switching apps :( – Jason Nam Dec 22 '15 at 06:51
  • That's exactly what I did, it didn't change a thing. My view is still moved according the the other app's keyboard. – dbmrq Dec 22 '15 at 06:59
  • You're right, though, I tried printing something at viewDidDisappear and it doesn't get called when switching apps. ApplicationDidEnterBackground is called too late, though, only after keyboardWillAppear is called for the other app and my view is moved. – dbmrq Dec 22 '15 at 07:06
  • Aha! `applicationWillResignActive` *is* called in time, though, and then everything works out! – dbmrq Dec 22 '15 at 07:10
  • Oh that sounds great! Good luck with your coding :) – Jason Nam Dec 22 '15 at 07:13
0

You can make sure that the view contains a first responder before doing any thing in keyboardWillShow. Using a UIView extension (or category) like this one -sorry couldn't find swift equivalent but you get the idea-, you can detect if any of the view's subviews is a first responder. I believe this should also work in situations like having 2 apps in the foreground at the same time on iOS 9.

Community
  • 1
  • 1
Ahmed Hamed
  • 504
  • 1
  • 3
  • 11
  • That would have been a cleaner solution, but I just tried it and it doesn't work. It's very weird, but apparently `keyboardWillAppear` is called for the other app before the selected text field resigns first responder. If I try to print `textField.isFirstResponder()`, then the view's offset, then `textField.isFirstResponder()` again, I get `true`, the offset and `false` one after the other very quickly. – dbmrq Dec 22 '15 at 07:59
0

I wasn't able to use the Application life Cycle methods but I was able to fix this problem for myself by checking if any of my textfields in the current view are firstResponder.

Updated With Code:

NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: UIResponder.keyboardWillShowNotification,object: nil)

@objc func keyboardWillShow(_ notification: Notification) {
    if storeNameTextField.isFirstResponder || methodTextField.isFirstResponder || amountTextField.isFirstResponder || dateTextField.isFirstResponder || categoryTextField.isFirstResponder {
        if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
            let keyboardRectangle = keyboardFrame.cgRectValue
            let keyboardHeight = keyboardRectangle.height
            self.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight - 22, right: 0)
        }
    }
}