I have a view controller with a UILabel in it that prints some words when a button is tapped. When the button is tapped, the navigation bar is set to hidden.
So I t
Set a constraint form the botton, lead and trail and one to a fixed height.
When you tell the navigation controller to hide the navigation bar, it resizes its content view (your ReadingViewController
's view) to be full-screen, and the content view lays out its subviews for the new full-screen size. By default, it does this layout outside of any animation block, so the new layout takes effect instantly.
To fix it, you need to make the view perform layout inside an animation block. Fortunately, the SDK includes a constant for duration of the animation that hides the navigation bar, and the animation uses a linear curve. Change your hideControls:
method to this:
- (void)hideControls:(BOOL)visible {
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{
[self.navigationController setNavigationBarHidden:visible animated:YES];
self.backFiftyWordsButton.hidden = visible;
self.forwardFiftyWordsButton.hidden = visible;
self.WPMLabel.hidden = visible;
self.timeRemainingLabel.hidden = visible;
[self.view layoutIfNeeded];
}];
}
There are two changes here. One is that I've wrapped the method body in an animation block using the UINavigationControllerHideShowBarDuration
constant, so the animation has the correct duration. The other change is that I send layoutIfNeeded
to the view, inside the animation block, so the views will animate to their new frames.
Here's the result:
You could also use this animation block to fade your labels in and out by changing their alpha
properties instead of their hidden
properties.
In response to the questions in your comment:
First, you need to understand the phases of the run loop. Your app is always running a loop on its main thread. The loop, extremely simplified, looks like this:
while (1) {
wait for an event (touch, timer, local or push notification, etc.)
Event phase: dispatch the event as appropriate (this often ends up
calling into your code, for example calling your tap recognizer's action)
Layout phase: send `layoutSubviews` to every view in the on-screen
view hierarchy that has been marked as needing layout
Draw phase: send `drawRect:` to any view that has been marked as needing
display (because it's a new view or it received `setNeedsDisplay` or
it has `UIViewContentModeRedraw`)
}
For example, if you put a breakpoint in hideControls:
, tap the screen, and then look at the stack trace in the debugger, you'll see PurpleEventCallback
way down in the trace (right above __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
). This tells you you're in the event handling phase. (Purple was the code name of the iPhone project inside Apple.)
If you see CA::Transaction::observer_callback
, you're in either the layout phase or the draw phase. Further up the stack you'll see either CA::Layer::layout_if_needed
or CA::Layer::display_if_needed
depending on which phase you're in.
So that's the run loop and its phases. Now, when does a view get marked as needing layout? It gets marked as needing layout when it receives setNeedsLayout
. You can send this if, for example, you've changed the content your views should display and they need to be moved or resized accordingly. But the view will send itself setNeedsLayout
automatically in two cases: when the size of its bounds
changes (or the size of its frame
), and when its subviews
array changes.
Note that changing the view's size or its subviews does not make the view lay out its subviews immediately! It's simply scheduled to lay out its subviews later, during the layout phase of the run loop.
So... what does this all have to do with you?
In your hideControls:
method, you do [self.navigationController setNavigationBarHidden:visible animated:YES]
. Suppose visible
is NO
. Here's what the navigation controller does in response:
The changes to the content view's frame cause the content view to send itself setNeedsLayout
.
Note that the changes to the navigation bar's frame and the content view's frame are animated. But the frames of the content view's subviews have not change yet. Those changes happen later, during the layout phase.
So the navigation controller animates the changes to your top-level content view, but it doesn't animate changes to the subviews of your content view. You have to force those changes to be animated.
You force those changes to be animated by taking two steps:
layoutIfNeeded
to the content view.The layoutIfNeeded documentation says this:
Use this method to force the layout of subviews before drawing. Starting with the receiver, this method traverses upward through the view hierarchy as long as superviews require layout. Then it lays out the entire tree beneath that ancestor.
It lays out the entire tree by sending layoutSubviews
messages to the views in the tree, in order from root to leaves. If you're not using auto layout, it also applies the autoresizing mask of each view's subviews before sending layoutSubviews
to the view.
So by sending layoutIfNeeded
to your content view, you are forcing auto layout to update the frames of your content view's subviews immediately, before layoutIfNeeded
returns. This means those changes happen inside your animation block, so they are animated with the same parameters (duration and curve) as the changes to the navigation bar and your content view.
Laying out subviews in an animation block is so important that Apple defined an animation option, UIViewAnimationOptionLayoutSubviews
. If you specify this option, then at the end of the animation block, it will automatically send layoutIfNeeded
. But using that option requires using the long version of the message, animateWithDuration:delay:options:animations:completion:
, so it's usually easier to just do [self.view layoutIfNeeded]
yourself at the end of the block.
(Copying my answer from the question you posted that is marked as duplicate: I have a UILabel positioned on the screen with autolayout, but when I hide the navigation bar it causes the label to "twitch" for a second)
Instead of bottom space constraint, you can try to define the top space constraint to the superview from the label (which is 22 in the constant), connect it as an IBOutlet to your view property, and animate it when the navigation bar is hidden or shown.
For example, I declare the top space property as topSpaceConstraint:
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *topSpaceConstraint;
Then inside the hideControls method, I can animate the constraint:
- (void)hideControls:(BOOL)visible {
if (visible) {
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{
self.topSpaceConstraint.constant = 66; //44 is the navigation bar height, you need to find a way not to hardcode this
[self.view layoutIfNeeded];
}];
}
else {
[UIView animateWithDuration:UINavigationControllerHideShowBarDuration animations:^{
self.topSpaceConstraint.constant = 22;
[self.view layoutIfNeeded];
}];
}
[self.navigationController setNavigationBarHidden:visible animated:YES];
self.backFiftyWordsButton.hidden = visible;
self.forwardFiftyWordsButton.hidden = visible;
self.WPMLabel.hidden = visible;
self.timeRemainingLabel.hidden = visible;
}