简体   繁体   中英

Using AutoLayout, how do I keep a UILabel in the same place when the navigation bar disappears?

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 tried taking the UILabel and giving it these constraints in Interface Builder:

在此输入图像描述

But with those, when I press the button, the UILabel jumps down with the nav bar disappearing, and then back up again, correcting itself, looking terrible. It should stay in its place permanently, no matter what goes on with the nav bar.

Here's a direct link to a short video showing what happens.

How would I best go about setting it so the UILabel stays in place?

Project: http://cl.ly/1T2K0V3w1P21

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.

UPDATE

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:

  • It begins an animation block.
  • It sets the position of the navigation bar to above the top of the screen.
  • It increases the height of the content view by 44 points (the height of the navigation bar).
  • It decreases the Y coordinate of the content view by 44 points.
  • It ends the animation block.

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:

  1. You create an animation block whose parameters match the parameters used by the navigation controller.
  2. Inside that animation block, you force the layout phase to happen immediately , by sending 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.

设置一个约束形成botton,lead和trail,一个约束到固定高度。

(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;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM