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):
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.
(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).
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 is0.35
during a push, and0.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
) andviewDidAppear
(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
andlayoutIfNeeded
, 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:
- viewDidLoad
- textField.becomeFirstResponder = true
- register for UIKeyboardWillShowNotification
- viewWillAppear
- UIKeyboardWillShowNotification is fired
- myConstraint.constant = keyboardHeight from notification
- layoutIfNeeded called inside an animation block
- 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.)