UIKeyboardDidShowNotification called multiple times, and sometimes with incorrect keyboard dimensions
Asked Answered
S

5

18

I am trying to move a UITextView above the keyboard whenever the keyboard appears/changes. Let's say I have the English keyboard displaying and then switch directly to the Chinese keyboard (which is taller than the standard English keyboard). In this scenario my text view always appears too high (to the naked eye, it looks like the text view is incorrectly offset by the size of the Chinese keyboard "input accessory view". I'd post an image but lack the reputation to do so :) ). I am adjusting my text view position when my app receives a UIKeyboardDidShowNotification (using UIKeyboardFrameEndUserInfoKey to get the height), and after some investigation UIKeyboardDidShowNotification is called multiple times, oftentimes with the incorrect keyboard dimensions (I've NSLogged the userInfo dictionary). I register for my keyboard notifications in ViewWillAppear and unregister in ViewWillDisappear. I am unable to determine what might be causing this notification to fire multiple times; my understanding is that this notification should only be fired one time, right after the keyboard finishes displaying. An additional note: I've NSLogged 'self' in the method that responds to UIKeyboardDidShowNotification and it is in fact always the same View Controller Object.

Even with this notification firing multiple times, however, I still do not understand why the keyboard height would be different for some of these notifications. One of the notifications always has the correct height, but when it is not the last notification fired the text view ends up in the wrong spot. Any insight on how to further troubleshoot would be much appreciated!

EDIT: The more I test the more it seems to be a problem with the Chinese keyboard specifically. Whenever I switch the keyboard from english to chinese I get three UIKeyboardWillShowNotifications:

2014-12-24 22:49:29.385 Example[1055:421943] info dictionary: {
UIKeyboardAnimationCurveUserInfoKey = 0;
UIKeyboardAnimationDurationUserInfoKey = 0;
UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 252}}";
UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 460}";
UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 442}";
UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 352}, {320, 216}}";
UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 316}, {320, 252}}";
}
2014-12-24 22:49:29.408 Example[1055:421943] info dictionary: {
UIKeyboardAnimationCurveUserInfoKey = 0;
UIKeyboardAnimationDurationUserInfoKey = 0;
UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 216}}";
UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 442}";
UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 460}";
UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 316}, {320, 252}}";
UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 352}, {320, 216}}";
}
2014-12-24 22:49:29.420 Example[1055:421943] info dictionary: {
UIKeyboardAnimationCurveUserInfoKey = 0;
UIKeyboardAnimationDurationUserInfoKey = 0;
UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {320, 288}}";
UIKeyboardCenterBeginUserInfoKey = "NSPoint: {160, 442}";
UIKeyboardCenterEndUserInfoKey = "NSPoint: {160, 424}";
UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 316}, {320, 252}}";
UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 280}, {320, 288}}";
}

The first one has the correct end height: 252. However, the next two are incorrect at 216 and 288. This happens reliably.

Here are a couple of snippets to demonstrate how I am managing subscriptions to notifications:

-(void)viewWillAppear:(BOOL)animated {

       [super viewWillAppear:animated];


       [self registerForKeyboardNotifications];


}

-(void)viewWillDisappear:(BOOL)animated {
       [super viewWillDisappear:animated];

       [[NSNotificationCenter defaultCenter] removeObserver:self
                                                name:UIKeyboardWillHideNotification
                                                object:nil];
       [[NSNotificationCenter defaultCenter] removeObserver:self
                                                name:UIKeyboardDidShowNotification
                                                object:nil];

}

- (void)registerForKeyboardNotifications {



      [[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardDidHide:)
                                             name:UIKeyboardWillHideNotification object:nil];

      [[NSNotificationCenter defaultCenter] addObserver:self      
                                         selector:@selector(keyboardDidShow:)                                       
                                             name:UIKeyboardDidShowNotification object:nil];
}
Shangrila answered 23/12, 2014 at 5:56 Comment(3)
I have identified the problem and a workaround. In my method that gets called in response to a UIKeyboardWillShowNotification I call [self.view layoutSubviews] within an animation block. When I animate layoutSubviews, additional UIKeyboardWillShowNotifications are triggered with the strange values posted in my question. If I call [self.view layoutSubviews] outside of an animation block, the notification is only triggered once and the value is always correct. So, without animation this works fine but I still do not understand why it does not work correctly with animation.Shangrila
I gave up trying to get it working with animation. But it does work consistently without, so I'm relatively satisfied with the solutionShangrila
Can you post keyboardDidHide() & keyboardDidShow() code?Perspicacious
L
8

If you're using the Cmd+K shortcut on the simulator to show/hide the keyboard, it may get called multiple times since that doesn't resign the textfield as the first responder.

If you instead use the keyboard's Return button then it will do the right thing and resign it.

Linkman answered 5/5, 2016 at 20:39 Comment(0)
D
5

The primary reason is that the your notification is invoked multiple times. For example, in swift, if you had the NSNotificationCenter.defaultCenter().addObserver(xx"keyboardWillShow"xx) inside viewWillAppear method and if you are going back and forth between the view then it will result in having multiple observer of the same "keyboardWillShow". Instead you should consider moving the addObserver call to "viewDidLoad()" method. Alternatively you can register/deregister observer when the view appears/disappears.

Dwaindwaine answered 20/1, 2016 at 19:39 Comment(1)
This is a good answer, but there's nothing Swift-specific about it.Topographer
S
0
- (void)keyboardDidAppear:(NSNotification *)note {
    CGSize keyboardSize = [note.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
    UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0);
    textView.contentInset = contentInsets;
    textView.scrollIndicatorInsets = contentInsets;
}

- (void)keyboardWillBeHidden:(NSNotification *)note {
    UIEdgeInsets contentInsets = UIEdgeInsetsZero;
    textView.contentInset = contentInsets;
    textView.scrollIndicatorInsets = contentInsets;
}

The solution is from Apple's official solution here like 曹 said.

Sibbie answered 7/3, 2015 at 13:46 Comment(0)
Q
0

In my case, I register for my keyboard notifications in ViewWillAppear and unregister in ViewWillDisappear too. But, It will cause the UIKeyboardDidShowNotification handler be fired multiple times. Seem like the unregister method do not work, so, every time the view appears, the counts of Observer for UIKeyboardDidShowNotification plus 1. Then, you touch inside UITextField, multiple Observer be notified, handler be fired again and again.

So, you must register for keyboard notifications in ViewDidLoad and don't unregister. Just like the Apple mentioned in this page,

// Call this method somewhere in your view controller setup code.

I think the 'setup' means ViewDidLoad.And In the Handling the keyboard notifications code list,no ViewWillDisappear.

Here's my handler for keyboard notify, and it work.

   func keyboardWillShow(notification: NSNotification) {
    let userInfo = notification.userInfo!
    let keyboardHeight = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue().height
    debugPrint("Before",NotifyView.frame)
    NotifyView.frame.offsetInPlace(dx: 0, dy: -keyboardHeight)
    debugPrint("After",NotifyView.frame)
}
func keyboardWillHide(notification: NSNotification) {//Do Nothing
}
Quackery answered 4/12, 2015 at 19:47 Comment(0)
T
0

I noticed that keyboardWillShowNotification is called twice sometimes when you long press on a UITextField so that the magnifying glass appears and then release it. In that case the first keyboardWillShowNotification shows an incorrect keyboard height while the height in the second notification is correct.

To avoid issues with animating the UI based on keyboard height I used constraints, so even if the animation block is called multiple times it will still produce a valid animation. We just need to call the following method in keyboard will show and will hide handler methods to get a correct animation:

func animateWithKeyboard(for notification: NSNotification, animation: ((CGRect) -> Void)?) {
    guard let frame = notification.keyboardEndFrame,
          let duration = notification.keyboardAnimationDuration,
          let curve = notification.keyboardAnimationCurve else {
      return
    }
    UIViewPropertyAnimator(duration: duration, curve: curve) {
      animation?(frame)
      self.view?.layoutIfNeeded()
    }.startAnimation()
  }

Here is also the extension of NSNotification used in the code above:

extension NSNotification {
  var keyboardAnimationDuration: Double? {
    userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
  }
  
  var keyboardAnimationCurve: UIView.AnimationCurve? {
    guard let curveValue = userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int else { return nil }
    return UIView.AnimationCurve(rawValue: curveValue)
  }

  var keyboardEndFrame: CGRect? {
    (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
  }
}
Thigmotropism answered 1/10, 2021 at 15:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.