使用 AutoLayout,当导航栏消失时,如何将 UILabel 保持在同一位置?

Posted

技术标签:

【中文标题】使用 AutoLayout,当导航栏消失时,如何将 UILabel 保持在同一位置?【英文标题】:Using AutoLayout, how do I keep a UILabel in the same place when the navigation bar disappears? 【发布时间】:2013-06-13 23:30:49 【问题描述】:

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

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

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

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

我最好如何设置它以使 UILabel 保持原位?

项目:http://cl.ly/1T2K0V3w1P21

【问题讨论】:

您可以设置从标签底部到屏幕底部的硬约束。 这不是图中第三个约束吗? 【参考方案1】:

当您告诉导航控制器隐藏导航栏时,它会将其内容视图(您的ReadingViewController 的视图)调整为全屏,并且内容视图将其子视图布置为新的全屏尺寸.默认情况下,它会在任何动画块外部进行此布局,因此新布局会立即生效。

要修复它,您需要让视图在动画块内执行布局。幸运的是,SDK 包含一个用于隐藏导航栏的动画持续时间的常量,并且动画使用线性曲线。将您的 hideControls: 方法更改为:

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

这里有两个变化。一个是我使用UINavigationControllerHideShowBarDuration 常量将方法主体包装在动画块中,因此动画具有正确的持续时间。另一个变化是我将layoutIfNeeded 发送到动画块内的视图,因此视图将动画到它们的新帧。

结果如下:

您还可以使用此动画块来淡入和淡出标签,方法是更改​​标签的 alpha 属性而不是 hidden 属性。

更新

回答您评论中的问题:

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

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`)


例如,如果您在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

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

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

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

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

layoutIfNeeded documentation 是这样说的:

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

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

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

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

【讨论】:

哇,太棒了,非常感谢。如果你不介意的话,有几个问题:我把模拟器放在慢动作中,我的问题似乎是标签立即跳到一个位置,然后慢慢地(沿着隐藏的导航栏)移动到一个新的位置。当你说它在动画块之外时,它是如何发挥作用的? (以及如何将它添加到动画块中解决这个问题?)你能解释一下 layoutIfNeeded,我真的很难理解它的作用。再次感谢您的精彩回答。 哇,这非常清楚,谢谢。我唯一剩下的问题是:首先是什么导致了跳跃?内容视图的移动?是因为所有的移动都发生在最后需要更改布局的那个块中吗? 跳转是在内容视图布局其子视图时引起的。请记住,标签的 Y 原点必须增加 44 点,因为内容视图变得高 44 点。标签框架的更改发生在任何动画块之外,因此它不是动画的。同时,内容视图框架的更改是动画的。因此,标签立即下跌了 44 点。内容视图向上移动了 44 个点,这平衡了标签视图的移动,但内容视图的移动是动画的。【参考方案2】:

从底部、前导和尾迹中设置一个约束,并将一个约束设置为固定高度。

【讨论】:

这不是图中第三个约束吗? opss.. 所以设置一个上限约束而不是固定高度,并控制标签设置为在中间绘制文本 您能说得更具体一点吗?我添加了顶部约束并删除了高度(现在看起来像这样:i.imgur.com/TDjkw1u.png)但是每次加载视图控制器时都会出现此错误:“<0x865dbe0 v:><0x8669dc0 v:><0x8669d80 v:><0x866c280 h="--&" v="--&" v:> 【参考方案3】:

(从您发布的标记为重复的问题中复制我的答案: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)

代替底部空间约束,您可以尝试从标签(常量中为 22)定义顶部空间约束到超级视图,将其作为 IBOutlet 连接到您的视图属性,并在导航栏时对其进行动画处理被隐藏或显示。

例如,我将顶部空间属性声明为 topSpaceConstraint:

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *topSpaceConstraint;

然后在 hideControls 方法中,我可以为约束设置动画:

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

【讨论】:

以上是关于使用 AutoLayout,当导航栏消失时,如何将 UILabel 保持在同一位置?的主要内容,如果未能解决你的问题,请参考以下文章

在 UISearchController 文本字段中键入时导航栏消失

当导航栏被点击时图像幻灯片消失

弹出到根视图控制器时导航栏消失

覆盖视图控制器的视图,包括导航栏,但是当我添加子视图时它消失了

导航栏消失时 UIStepper 不会向上移动

如何防止 UIImageView 使用 Autolayout 调整大小