本文介绍了使用 AutoLayout,如何在导航栏消失时将 UILabel 保持在同一位置?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个带有 UILabel 的视图控制器,当点击按钮时会打印一些单词.点击按钮时,导航栏设置为隐藏.

所以我尝试使用 UILabel 并在 Interface Builder 中给它这些约束:

但是有了这些,当我按下按钮时,UILabel 会随着导航栏的消失而跳下,然后又回来,自我纠正,看起来很糟糕.无论导航栏发生什么,它都应该永久保留在其位置.

您还可以通过更改标签的 alpha 属性而不是 hidden 属性来使用此动画块来淡入和淡出标签.

更新

回答您评论中的问题:

首先,您需要了解运行循环的各个阶段.您的应用程序始终在其主线程上运行一个循环.极其简化的循环如下所示:

while (1) {等待事件(触摸、计时器、本地或推送通知等)事件阶段:根据需要调度事件(这通常会结束调用您的代码,例如调用您的点击识别器的操作)布局阶段:将 `layoutSubviews` 发送到屏幕上的每个视图已标记为需要布局的视图层次结构绘制阶段:将`drawRect:`发送到已标记为需要的任何视图显示(因为它是一个新视图或者它收到了 `setNeedsDisplay` 或它有`UIViewContentModeRedraw`)}

例如,如果你在 hideControls: 中放置一个断点,点击屏幕,然后在调试器中查看堆栈跟踪,你会看到 PurpleEventCallback 方式在跟踪中(__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ 正上方).这告诉您您正处于事件处理阶段.(紫色是苹果内部 iPhone 项目的代号.)

如果您看到 CA::Transaction::observer_callback,则说明您处于布局阶段或绘制阶段.在堆栈的上方,您会看到 CA::Layer::layout_if_neededCA::Layer::display_if_needed,具体取决于您所处的阶段.

这就是运行循环及其阶段.现在,视图何时被标记为需要布局?它在收到 setNeedsLayout 时被标​​记为需要布局.例如,如果您更改了视图应显示的内容并且需要相应地移动或调整它们的大小,则可以发送此信息.但是视图会在两种情况下自动发送自己的 setNeedsLayout:当它的 bounds 的大小改变(或者它的 frame 的大小),和当它的 subviews 数组改变时.

请注意,更改视图的大小或其子视图不会使视图立即布置其子视图!它只是安排稍后在运行循环的布局阶段布局其子视图.

那么……这一切与你有什么关系?

在您的 hideControls: 方法中,您执行 [self.navigationController setNavigationBarHidden:visible animated:YES].假设 visibleNO.以下是导航控制器的响应:

  • 它开始一个动画块.
  • 它将导航栏的位置设置为屏幕顶部的上方.
  • 它将内容视图的高度增加 44 磅(导航栏的高度).
  • 它将内容视图的 Y 坐标减少 44 个点.
  • 它结束了动画块.

对内容视图框架的更改导致内容视图向自身发送setNeedsLayout.

请注意,导航栏框架和内容视图框架的更改是动画的.但是内容视图的子视图的框架还没有改变.这些更改发生在稍后的布局阶段.

因此,导航控制器会为您的顶级内容视图的更改设置动画,但不会为您的内容视图的子视图设置动画.您必须强制使这些更改变为动画.

您可以通过两个步骤强制使这些更改变为动画:

  1. 您创建一个动画块,其参数与导航控制器使用的参数相匹配.
  2. 在该动画块中,您可以通过向内容视图发送 layoutIfNeeded 来强制布局阶段立即发生.

layoutIfNeeded 文档 这么说:

使用此方法在绘制之前强制子视图的布局.从接收者开始,只要超级视图需要布局,此方法就会向上遍历视图层次结构.然后它将整个树放在该祖先的下方.

它通过向树中的视图发送 layoutSubviews 消息来布局整个树,按从根到叶的顺序排列.如果您不使用自动布局,它还会在将 layoutSubviews 发送到视图之前应用每个视图子视图的自动调整大小掩码.

因此,通过将 layoutIfNeeded 发送到您的内容视图,您将强制自动布局在 layoutIfNeeded 返回之前立即更新内容视图的子视图的框架.这意味着这些更改发生在您的动画块内,因此它们使用与导航栏和内容视图更改相同的参数(持续时间和曲线)进行动画处理.

在动画块中布置子视图非常重要,Apple 定义了一个动画选项,UIViewAnimationOptionLayoutSubviews.如果你指定了这个选项,那么在动画块的最后,它会自动发送layoutIfNeeded.但是使用该选项需要使用长版本的消息,animateWithDuration:delay:options:animations:completion:,所以通常更容易做到 [self.view layoutIfNeeded] 你自己在街区的尽头.

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:

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.

这篇关于使用 AutoLayout,如何在导航栏消失时将 UILabel 保持在同一位置?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-31 01:18