8

Normally if you set a UITextField to become first responder in viewDidLoad or viewWillAppear, when the view controller is pushed onto the navigation stack, you get an animation where the view controller slides in from the right with the keyboard already up (i.e. there is no double animation):

enter image description here
Ideal animation (250 ms)

However, when I use auto layout and control the layout of my view depending on the keyboard size it causes weird animations. In this case my button has a constraint to the bottom of the superview and I update its constant property in the keyboard notification event.

enter image description here
Buggy animations (250 ms)

(Notice how the button, the placeholder, and the label are all animating upwards during the push. Also notice how the cursor begins below the placeholder text and floats upwards until it reaches the placeholder.)

The underlying problem seems to be that when the notification is fired, it is already in an animation block (the one that animates the navigation controller), you then fire off another animation block inside the existing one (i.e. self.view.layoutIfNeeded() so that auto layout changes are animated).

Of course I could call becomeFirstResponder in viewDidAppear, but then I get two animations. I want to utilize the keyboard-already-up behavior so the interface looks more responsive (ideally keeping it near 250 ms).

enter image description here
Double animation (600 ms)

How can I handle the UIKeyboardWillShowNotification in such a way that it will work for both when the user taps on a UITextField and, when pushing a view controller, uses my ideal animation above?


Ideal solution:

  • Apple would provide another value in the notification dictionary such as shouldAnimate, which would tell you if you should use a UIAnimation block or not when handling the notification. Ex: in the case of this push, it would send false, but if the user just taps on a text field, it would send true.

Hacky workarounds:

  • Instead of setting the becomeFirstReponder in viewDidLoad immediately, call it with an arbitrary delay. This will cause an undesirable double animation: first the view controller will slide in from the right, and then the keyboard will appear. (See "Double animation" animation above for an example of this.)
  • Deduce if you are in an animation block by looking at a key in the notification's user info dictionary, such as UIKeyboardAnimationDurationUserInfoKey which is 0.35 during a push, and 0.25 when the user taps on a UITextField. This can easily break if animations are customized, Apple changes animations, etc.
  • Set a flag in viewWillAppear (preventAnimationBlock = true) and viewDidAppear (preventAnimationBlock = false). This seems like the safest approach and produces the exact animation I want. However, maintaining state is undesirable.

Solutions that didn't work:

  • Layout in viewDidLoad. At the end of view did load, call setNeedsLayout and layoutIfNeeded, while this solution has worked in the past, it doesn't work with the button example above. It will cause the label and text view to not animate correctly, but the button will still animate incorrectly.
  • Attempt to check if you are already in animation block: If there were a way to know if I'm inside an animation block already, it may solve my problem; I would simply not call layoutIfNeeded at all in that case. I tried figuring that out using one of these methods, but none of them worked. Probably because this is some kind of hidden/magic animation block that Apple uses internally.
  • Attempt to see if the view is on screen: The thinking with this one is that the view would report itself offscreen because viewDidAppear has not been called yet, and so I would not wrap the code in an animation block. However, when you attempt to find out if the view is on screen by looking at its window property, it reports that it is on screen, and thus this solution would not work.

Reference:

Order of events:

  1. viewDidLoad
    • textField.becomeFirstResponder = true
    • register for UIKeyboardWillShowNotification
  2. viewWillAppear
  3. UIKeyboardWillShowNotification is fired
    • myConstraint.constant = keyboardHeight from notification
    • layoutIfNeeded called inside an animation block
  4. viewDidAppear

Notification info when the user taps on a textField:

[UIKeyboardFrameBeginUserInfoKey: NSRect: {{0, 736}, {414, 271}},
UIKeyboardCenterEndUserInfoKey: NSPoint: {207, 600.5},
UIKeyboardBoundsUserInfoKey: NSRect: {{0, 0}, {414, 271}},
UIKeyboardFrameEndUserInfoKey: NSRect: {{0, 465}, {414, 271}},
UIKeyboardAnimationDurationUserInfoKey: 0.25,
UIKeyboardCenterBeginUserInfoKey: NSPoint: {207, 871.5},
UIKeyboardAnimationCurveUserInfoKey: 7,
UIKeyboardIsLocalUserInfoKey: 1]

Notification info when calling textField.becomeFirstResponder in viewDidLoad (i.e. during a push):

[UIKeyboardFrameBeginUserInfoKey: NSRect: {{0, 465}, {414, 271}},
UIKeyboardCenterEndUserInfoKey: NSPoint: {207, 600.5},
UIKeyboardBoundsUserInfoKey: NSRect: {{0, 0}, {414, 271}},
UIKeyboardFrameEndUserInfoKey: NSRect: {{0, 465}, {414, 271}},
UIKeyboardAnimationDurationUserInfoKey: 0.35,
UIKeyboardCenterBeginUserInfoKey: NSPoint: {207, 600.5},
UIKeyboardAnimationCurveUserInfoKey: 7,
UIKeyboardIsLocalUserInfoKey: 1]

(Differences in bold.)

Community
  • 1
  • 1
Senseful
  • 86,719
  • 67
  • 308
  • 465
  • 2
    Why are you changing the constraint at all? [Apple's recommended solution for managing content that is under the keyboard](https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html#//apple_ref/doc/uid/TP40009542-CH5-SW7) is to set the `contentInset.bottom` of your scroll view (or table view in this case), not to change the size of the view. If your view controller is a subclass of `UITableViewController`, it will take care of setting `contentInset.bottom` for you. – rob mayoff Nov 02 '15 at 20:35
  • @robmayoff: The table view was just chose as an example, I updated the question to show something more realistic. – Senseful Nov 02 '15 at 22:10
  • "Normally if you set a UITextField to become first responder in `viewDidLoad`" Actually, one might argue that this is wrong. A text field can hardly be first responder when it is not even in the interface — and in `viewDidLoad`, the view and its subviews are not yet in the interface. Personally, I think that what you call the double animation — push, then show the keyboard — is correct. And of course if you did that, your problems would be over. – matt Nov 02 '15 at 22:51
  • @matt: Good point, people prefer different animations. I updated the question so hopefully it's less subjective. – Senseful Nov 02 '15 at 23:38
  • Thanks a lot for the breakdown! Very useful. Also look into viewWill/DidLayoutSubviews. – Stian Høiland Nov 18 '15 at 15:49

0 Answers0