我有一个带有UILabel的视图控制器,当按下按钮时会打印一些单词。点击按钮时,导航栏设置为隐藏。
所以我尝试使用UILabel并在Interface Builder中给出这些约束:
但是有了这些,当我按下按钮时,UILabel跳下来,导航栏消失,然后再次备份,纠正自己,看起来很糟糕。无论导航栏发生什么,它都应永久保留在原位。
Here's a direct link to a short video showing what happens.
我如何最好地设置它以便UILabel保持原位?
答案 0 :(得分:8)
当您告诉导航控制器隐藏导航栏时,它会将其内容视图(您的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__
之上。这告诉您,您处于事件处理阶段。 (Purple是Apple内部iPhone项目的代号。)
如果您看到CA::Transaction::observer_callback
,则您处于布局阶段或绘制阶段。在堆栈的上方,您会看到CA::Layer::layout_if_needed
或CA::Layer::display_if_needed
,具体取决于您所处的阶段。
这就是运行循环及其阶段。现在,视图何时被标记为需要布局?它在收到setNeedsLayout
时被标记为需要布局。例如,如果您更改了视图应显示的内容并且需要相应地移动或调整其大小,则可以发送此内容。但是视图会在两种情况下自动发送setNeedsLayout
:当bounds
的大小发生变化(或frame
}的大小时,以及subviews
数组更改时
请注意,更改视图的大小或其子视图不会使视图立即显示其子视图!它只是安排在运行循环的布局阶段之后布置其子视图。
那么......这与你有什么关系?
在hideControls:
方法中,您执行[self.navigationController setNavigationBarHidden:visible animated:YES]
。假设visible
是NO
。这是导航控制器响应的内容:
对内容视图框架的更改会导致内容视图自行发送setNeedsLayout
。
请注意,对导航栏框架和内容视图框架的更改是动画的。但是内容视图的子视图的框架还有不更改。这些变化发生在布局阶段的后期。
因此,导航控制器会将更改设置为您的顶级内容视图的动画,但它不会对内容视图的子视图的更改进行动画处理。您必须强制将这些更改设置为动画。
您可以通过以下两个步骤强制对这些更改进行动画处理:
layoutIfNeeded
发送到内容视图来强制布局阶段立即。 layoutIfNeeded
documentation说:
使用此方法在绘制前强制子视图的布局。从接收者开始,只要superviews需要布局,此方法就会向上遍历视图层次结构。然后它展示了祖先下面的整棵树。
它通过向树中的视图发送layoutSubviews
消息按照从根到叶的顺序来布局整个树。如果您未使用自动布局,则在将layoutSubviews
发送到视图之前,它还会应用每个视图子视图的自动调整屏幕。
因此,通过向您的内容视图发送layoutIfNeeded
,您会强制自动布局在layoutIfNeeded
返回之前立即更新内容视图的子视图的帧。这意味着这些更改发生在动画块内,因此它们使用与导航栏和内容视图的更改相同的参数(持续时间和曲线)进行动画处理。
在动画块中布置子视图非常重要,Apple定义了动画选项UIViewAnimationOptionLayoutSubviews
。如果指定此选项,则在动画块结束时,它将自动发送layoutIfNeeded
。但是使用该选项需要使用邮件的长版本animateWithDuration:delay:options:animations:completion:
,因此通常在块结束时自己[self.view layoutIfNeeded]
更容易。
答案 1 :(得分:1)
从底部,引线和轨迹设置约束,将一个约束设置为固定高度。
答案 2 :(得分:1)
(从我发布的标记为重复的问题中复制我的答案: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;
}