同时更新多个视图约束
Posted
技术标签:
【中文标题】同时更新多个视图约束【英文标题】:Updating multiple view constraints at the same time 【发布时间】:2014-07-14 22:51:13 【问题描述】:有没有办法在运行时告诉自动布局引擎我要同时更改视图上的多个约束?我一直遇到这样一种情况,即我更新约束的顺序很重要,因为即使说完所有的约束条件都满足了,在另一个抛出 LAYOUT_CONSTRAINTS_NOT_SATISFIABLE 异常(或任何异常)之前更改一个常量其实是叫...)
这是一个例子。我创建一个视图并将其添加到当前视图并对其设置一些约束。我正在保存前沿和后沿约束,以便稍后更改它们的常量。
- (MyView*)createMyView
MyView* myView = [[MyView alloc init];
[myView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.mainView addSubview:myView];
NSDictionary* views = @@"myView" : myView;
NSLayoutConstraint* leading = [NSLayoutConstraint constraintWithItem:myView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1.0f
constant:0];
NSLayoutConstraint* trailing = [NSLayoutConstraint constraintWithItem:myView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTrailing
multiplier:1.0f
constant:0];
myView.leadingConstraint = leading;
myView.trailingConstraint = trailing;
[self.view addConstraints:@[leading, trailing]];
NSString* vflConstraints = @"V:|[myView]|";
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:vflConstraints
options:NSLayoutFormatAlignAllCenterX
metrics:nil
views:views]];
return myView;
当视图上的滑动手势触发时,我会根据滑动方向在一侧或另一侧创建一个新视图。然后我更新约束并设置动画,以便新的传入视图将旧视图“推”出视图。
- (void)transitionToCameraView:(MyView*)newCameraView
swipeDirection:(UISwipeGestureRecognizerDirection)swipeDirection
// Set new view off screen before adding to parent view
float width = self.myView.frame.size.width;
float height = self.myView.frame.size.height;
float initialX = (swipeDirection == UISwipeGestureRecognizerDirectionLeft) ? width : -width;
float finalX = (swipeDirection == UISwipeGestureRecognizerDirectionLeft) ? -width : width;
float y = 0;
[newView setFrame:CGRectMake(initialX, y, width, height)];
[self.mainView addSubview:newView];
self.myView.leadingConstraint.constant = finalX;
self.myView.trailingConstraint.constant = finalX;
// Slide old view out and new view in
[UIView animateWithDuration:0.4f
delay:0.0f
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
animations:^
[newView setFrame:self.myView.frame];
[self.myView layoutIfNeeded];
completion:^(BOOL finished)
[self.myView removeFromSuperview];
self.myView = newView;
];
当滑动方向为 UISwipeGestureRecognizerDirectionLeft 时,这可以正常工作,但当它为 UISwipeGestureRecognizerDirectionRight 时,会抛出异常
self.myView.leadingConstraint.constant = finalX;
如果我检查方向并以相反的顺序更新约束,一切都很好并且不会抛出异常:
if(swipeDirection == UISwipeGestureRecognizerDirectionLeft)
self.myView.leadingConstraint.constant = finalX;
self.myView.trailingConstraint.constant = finalX;
else
self.myView.trailingConstraint.constant = finalX;
self.myView.leadingConstraint.constant = finalX;
这对我来说很有意义为什么抛出异常,但似乎更改多个约束参数是我们应该能够做的事情,并让自动布局引擎知道我们将要这样做,而不是它立即试图满足约束并呕吐。
有没有办法告诉自动布局引擎我正在对多个约束进行更改,然后告诉它在我完成后再次运行,还是我坚持这样做?当我完成更改后一切实际上都会好起来时,在这里更新约束的顺序对我来说似乎很愚蠢。
编辑 经过进一步调查,我发现订单重要的根本原因是 myView 添加了一个子视图,该子视图具有指定其前沿距 myView 前沿 10pt 的约束。如果我对新常量进行调整以解决此问题,则不再存在约束冲突,也没有例外。
self.myView.leadingConstraint.constant = finalX+25; // Just arbitrary number that happened to work
self.myView.trailingConstraint.constant = finalX;
出现问题的原因是从左到右滑动时的新约束将myView折叠为宽度为0。显然在宽度为0的视图中没有足够的空间添加10pt的前沿约束到一个子视图。相反,以相反的顺序更改常量,首先通过增加后缘来扩大视野,然后通过减少前缘再次缩小视野。
更有理由告诉自动布局系统我们将进行多项更改。
【问题讨论】:
【参考方案1】:要同时更新多个约束,请使用更新逻辑覆盖 updateConstraints
并调用 setNeedsUpdateConstraints
以触发更新。来自 Apple 的Auto Layout Guide > Advanced Auto Layout > Changing Constraints:
要批量更改,而不是直接进行更改,调用 持有约束的视图上的
setNeedsUpdateConstraints
方法。 然后,覆盖视图的updateConstraints
方法来修改 受影响的约束。
以这种方式更新约束并不那么简单,因此我建议您在使用此方法之前仔细阅读文档。
【讨论】:
很好的答案,我在尝试一个一个地更新 2 个约束时遇到了 Ambiguous layout 错误,但是当我开始使用updateConstraints
时问题就消失了。【参考方案2】:
首先,您似乎对myView
相对于它的超级视图有过度约束,以至于约束不能让它滑动。您已经固定了它的前缘和后缘,此外,在添加垂直约束时,您还通过指定 NSLayoutFormatAlignAllCenterX
固定了它的中心。
除此之外,考虑它的更好方法可能是拥有两个必须同步更新的约束表明您使用了错误的约束。您可以对前沿设置一个约束,然后设置一个约束使myView
的宽度等于self.view
的宽度。这样一来,您只需调整前导约束的常数,两个边都会随之移动。
另外,您正在使用-setFrame:
,这是自动布局的禁忌。您应该为约束设置动画。您应该对newView
设置约束,使其前缘或后缘(视情况而定)等于myView
的相邻边缘,并且其宽度等于self.view
的宽度。然后,在动画块中,更改确定myView
(以及间接newView
)位置的约束常数,以将它们滑动到一起。在完成处理程序中,在 newView
上安装一个约束以保持其前沿与 self.view
的一致,因为它是新的 myView
。
BOOL left = (swipeDirection == UISwipeGestureRecognizerDirectionLeft);
float width = self.myView.frame.size.width;
float finalX = left ? -width : width;
NSString* layout = left ? @"[myView][newView(==mainView)]" : @"[newView(==mainView)][myView]";
NSDictionary* views = NSDictionaryOfVariableBindings(newView, myView, mainView);
[self.mainView addSubview:newView];
[self.mainView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:layout
options:0
metrics:nil
views:views]];
[self.mainView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|newView|"
options:0
metrics:nil
views:views]];
[self.mainView layoutIfNeeded];
// Slide old view out and new view in
[UIView animateWithDuration:0.4f
delay:0.0f
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
animations:^
self.myView.leadingConstraint.constant = finalX;
[self.myView layoutIfNeeded];
completion:^(BOOL finished)
[self.myView removeFromSuperview];
self.myView = newView;
NSLayoutConstraint* leading = [NSLayoutConstraint constraintWithItem:newView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeLeading
multiplier:1.0f
constant:0];
self.myView.leadingConstraint = leading;
[self.mainView addConstraint:leading];
];
已编辑以更直接地回答问题:不,公共 API 中无法批量约束更新,因此引擎不会在添加或更改它们时一次一个地检查它们,通过设置它们常数。
【讨论】:
感谢您的回答,但这根本不能回答我的问题,即“有没有办法告诉自动布局我即将对多个约束进行更改。” 当您指定(否则)垂直约束时,如果您从选项中省略NSLayoutFormatAlignAllCenterX
,您确定仍然会出现异常吗?我不确定这个例外仅仅是因为你设置前导和尾随常量的顺序。
是的,我确定。在此特定实例中将 options:
设置为什么并不重要。因为只有一个视图,并且它的所有边缘都固定在父视图的边缘,所以无论如何它都与所有内容对齐。将其更改为其他内容仍会导致引发异常。我已经发现了订单为何重要的根本原因。请参阅我对原始问题的编辑。
我已编辑我的答案以更直接地解决您的问题。 ;-P
好的,那我就接受吧。感谢您的对话。此外,我 DO 喜欢您只需更改一个约束即可获得幻灯片动画的解决方案。也谢谢你。【参考方案3】:
此外,您可以做的是将其中一个约束设置为非活动,并在设置正确后再次激活它:
self.constraint.active = false;
【讨论】:
【参考方案4】:根据 Dannie P 的回答:
// before the updates
[self.view.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop)
obj.active = NO;
];
// perform updates
// after the updates
[self.view.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop)
obj.active = YES;
];
【讨论】:
【参考方案5】:这对我有用:
NSLayoutConstraint.deactivate([constraint1, constraint2])
constraint1.constant -= k
constraint2.constant += k
NSLayoutConstraint.activate([constraint1, constraint2])
【讨论】:
【参考方案6】:我遇到了一个类似的问题,我需要以编程方式将视图(具有固定高度)粘贴到其父视图的顶部 XOR 到底部,同时编写尽可能少的代码。这里的重点是找到一种方法来切换两个互斥的强制约束(意味着两个不能同时激活,但一个是强制的)。
我的解决方案是简单地降低 IB 中的一个约束优先级。因此,当停用活动约束而另一个尚未激活时(反之亦然)不会触发约束问题。
let isViewUpward = pointInView.y < view.bounds.height / 2
topConstraint.isActive = isViewUpward
bottomConstraint.isActive = !isViewUpward
【讨论】:
以上是关于同时更新多个视图约束的主要内容,如果未能解决你的问题,请参考以下文章