具有隐藏 UIView 的自动布局? ?

我觉得根据业务逻辑,显示/隐藏 UIViews(通常是 UILabels)是一种相当常见的范例。我的问题是,什么是最好的方式使用自动布局来响应隐藏的视图,如果他们的框架是0x0。下面是一个包含1-3个特性的动态列表示例。

Dynamic features list

现在我有一个10px 的顶部空间从按钮到最后一个标签,这显然不会滑动时,标签是隐藏的。从现在开始,我创建了一个这个约束的出口,并根据显示的标签数量修改常量。这显然是有点古怪,因为我使用负常量值推动按钮在隐藏的帧。它也不好,因为它没有被限制到实际的布局元素,只是基于已知的高度/其他元素的填充的鬼鬼祟祟的静态计算,并且显然与 AutoLayout 的构建目的相抵触。

显然,我可以根据我的动态标签创建新的约束,但是这需要很多微观管理和冗长的工作,因为我试图只是压缩一些空格。还有更好的方法吗?更改框架大小0,0和让自动布局做它的事情没有操作的约束?完全移除视图?

实际上,只需要从隐藏视图的上下文中修改常量,就需要一行代码和简单的计算。用 constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:重新创建新的约束似乎很繁重。

编辑2018年2月 : 见本的 UIStackView回答

99334 次浏览

我个人对显示/隐藏视图的偏好是创建一个具有适当宽度或高度约束的 IBOutlet。

然后,我将 constant值更新为 0以隐藏,或者显示应该显示的值。

这种技术的最大优点是可以维护相对约束。例如,假设视图 A 和视图 B 的水平间隙为 X。当视图 A 宽度 constant设置为 0.f时,视图 B 将向左移动以填充该空间。

不需要添加或删除约束,这是一个重量级操作。只需更新约束的 constant就可以解决这个问题。

我构建分类以便轻松更新约束:

[myView1 hideByHeight:YES];

回答这里: 隐藏自动布局 UIView: 如何获得现有的 NSLayoutConstraint 来更新这个

enter image description here

令我感到惊讶的是,UIKit没有为这种期望的行为提供更优雅的方法。这似乎是一个非常普遍的事情,希望能够做到。

由于将约束连接到 IBOutlets并将它们的常量设置为 0让人感到恶心(并且当您的视图具有子视图时会导致 NSLayoutConstraint警告) ,因此我决定创建一个提供 隐藏/显示具有 Auto Layout 约束的 UIView的简单有状态方法的扩展

它只是隐藏视图并删除外部约束。当您再次显示视图时,它会将约束添加回来。唯一需要注意的是,您需要为周围的视图指定灵活的故障转移约束。

剪辑 这个答案针对的是 iOS8.4及以下版本,在 iOS9中,只需使用 UIStackView方法。

隐藏时使用一个常量 0,再次显示时使用另一个常量的解决方案是功能性的,但如果内容大小灵活,就不能令人满意了。你需要衡量你的灵活的内容,并设置一个常数回来。如果内容因为服务器或 UI 事件而改变大小,那么就会出现问题。

我有个更好的办法。

这个想法是当我们隐藏元素时,设置0高度规则为高优先级,这样它就不会占用自动布局空间。

你可以这样做:

设置一个宽度(或高度)为0的低优先级 Interface Builder。

setting width 0 rule

Interface Builder 不会对冲突大喊大叫,因为优先级很低。通过临时将优先级设置为999来测试高度行为(1000被禁止以编程方式进行变异,因此我们不会使用它)。Interface Builder 现在可能会大声疾呼相互冲突的限制。您可以通过将相关对象的优先级设置为900左右来修复这些问题。

2. 添加一个出口,以便在代码中修改宽度约束的优先级:

outlet connection

3. 隐藏元素时调整优先级:

cell.alertTimingView.hidden    = place.closingSoon != true
cell.alertTimingWidth.priority = place.closingSoon == true ? 250 : 999

对于 iOS9 + 来说,UIStackView 可能是一个不错的选择。它不仅可以处理隐藏视图,如果设置正确,还可以删除额外的间距和边距。

子类化视图并覆盖 func intrinsicContentSize() -> CGSize。如果视图被隐藏,只需返回 CGSizeZero

我也会提供我的解决方案,提供多样性。)我认为,为每个项目的宽度/高度加上间距创建一个出口是荒谬的,并打击了代码,可能的错误和数量的复杂性。

我的方法移除所有视图(在我的例子中是 UIImageView 实例) ,选择哪些视图需要添加回来,在循环中,它将每个视图添加回来并创建新的约束。其实很简单,请跟进。下面是我的简洁代码:

// remove all views
[self.twitterImageView removeFromSuperview];
[self.localQuestionImageView removeFromSuperview];


// self.recipients always has to be present
NSMutableArray *items;
items = [@[self.recipients] mutableCopy];


// optionally add the twitter image
if (self.question.sharedOnTwitter.boolValue) {
[items addObject:self.twitterImageView];
}


// optionally add the location image
if (self.question.isLocal) {
[items addObject:self.localQuestionImageView];
}


UIView *previousItem;
UIView *currentItem;


previousItem = items[0];
[self.contentView addSubview:previousItem];


// now loop through, add the items and the constraints
for (int i = 1; i < items.count; i++) {
previousItem = items[i - 1];
currentItem = items[i];


[self.contentView addSubview:currentItem];


[currentItem mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(previousItem.mas_centerY);
make.right.equalTo(previousItem.mas_left).offset(-5);
}];
}




// here I just connect the left-most UILabel to the last UIView in the list, whichever that was
previousItem = items.lastObject;


[self.userName mas_remakeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(previousItem.mas_left);
make.leading.equalTo(self.text.mas_leading);
make.centerY.equalTo(self.attachmentIndicator.mas_centerY);;
}];

我得到干净,一致的布局和间距。我的代码使用砌体,我强烈推荐它: https://github.com/SnapKit/Masonry

在本例中,我将 Author 标签的高度映射到适当的 IBOutlet:

@property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;

当我将约束的高度设置为0.0 f 时,我们保留了“填充”,因为 Play 按钮的高度允许这样做。

cell.authorLabelHeight.constant = 0; //Hide
cell.authorLabelHeight.constant = 44; //Show

enter image description here

我刚刚发现,要让 UILabel 不占用空间,必须隐藏它,并将其文本设置为空字符串。(iOS9)

了解这个事实/bug 可以帮助一些人简化他们的布局,甚至可能是原始问题的布局,所以我想我应该发布它。

最佳实践是,一旦所有内容都具有正确的布局约束,就添加高度或约束,具体取决于您希望周围视图如何移动并将约束连接到 IBOutlet属性。

确保属性为 strong

在代码中你只需要将常量设置为0,并且 启动它,以隐藏内容,或者 解除它来显示内容。 这比把常量值搞乱保存-恢复它要好。 事后别忘了打电话给 layoutIfNeeded

如果要隐藏的内容是分组的,最佳实践是将所有内容放入一个视图并向该视图添加约束

@property (strong, nonatomic) IBOutlet UIView *myContainer;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *myContainerHeight; //should be strong!!

-(void) showContainer
{
self.myContainerHeight.active = NO;
self.myContainer.hidden = NO;
[self.view layoutIfNeeded];
}
-(void) hideContainer
{
self.myContainerHeight.active = YES;
self.myContainerHeight.constant = 0.0f;
self.myContainer.hidden = YES;
[self.view layoutIfNeeded];
}

安装完成后,可以在 IntefaceBuilder 中测试它,方法是将约束设置为0,然后返回到原始值。不要忘记检查其他约束优先级,这样当隐藏时就没有冲突了。另一种测试方法是将其设置为0并将优先级设置为0,但是,您不应该忘记再次将其恢复为最高优先级。

我喜欢的方法和 Jorge Arimany 建议的非常相似。

我更喜欢创建多个约束。首先创建第二个标签可见时的约束。为按钮和第二个标签之间的约束创建一个出口(如果您使用 objecc,请确保其强大)。此约束确定按钮和第二个标签之间的高度(当它可见时)。

然后创建另一个约束,指定隐藏第二个按钮时按钮和顶部标签之间的高度。创建到第二个约束的插座,并确保该插座有一个强指针。然后在 Interface Builder 中取消选中 installed复选框,并确保第一个约束的优先级低于第二个约束的优先级。

最后,当隐藏第二个标签时,切换这些约束的 .isActive属性并调用 setNeedsDisplay()

就是这样,没有魔术数字,没有数学,如果你有多个约束,打开和关闭,你甚至可以使用插座集合,以保持它们的组织状态。(也就是说,将所有隐藏约束保留在一个 OutletCollection 中,而非隐藏约束保留在另一个 OutletCollection 中,然后迭代每个集合,切换它们的。正在运作状态)。

我知道 Ryan Romanchuk 说他不想使用多个约束,但是我觉得这不是微观管理,而是更简单的动态创建视图和编程约束(这是我认为他想要避免的,如果我没有读错这个问题)。

我创造了一个简单的例子,我希望它是有用的..。

import UIKit


class ViewController: UIViewController {


@IBOutlet var ToBeHiddenLabel: UILabel!




@IBOutlet var hiddenConstraint: NSLayoutConstraint!
@IBOutlet var notHiddenConstraint: NSLayoutConstraint!




@IBAction func HideMiddleButton(_ sender: Any) {


ToBeHiddenLabel.isHidden = !ToBeHiddenLabel.isHidden
notHiddenConstraint.isActive = !notHiddenConstraint.isActive
hiddenConstraint.isActive = !hiddenConstraint.isActive


self.view.setNeedsDisplay()
}
}

enter image description here

这里有很多解决方案,但我通常的方法又不一样了:)

设置两组约束,类似于 Jorge Arimany 和 TMin 的答案:

enter image description here

所有三个标记约束对常量具有相同的值。标记为 A1和 A2的约束的优先级设置为500,而标记为 B 的约束的优先级设置为250(或代码中的 UILayoutProperty.defaultLow)。

将约束 B 连接到 IBOutlet。然后,在隐藏元素时,只需将约束优先级设置为高(750) :

constraintB.priority = .defaultHigh

但是当元素可见时,将优先级设置回 low (250) :

constraintB.priority = .defaultLow

与仅仅更改约束 B 的 isActive相比,这种方法的(不可否认的是次要的)优势在于,如果通过其他方式从视图中删除了瞬态元素,那么仍然有一个工作约束。

尝试 BoxView,它使动态布局简洁易读。
对你来说是这样的:

    boxView.optItems = [
firstLabel.boxed.useIf(isFirstLabelShown),
secondLabel.boxed.useIf(isSecondLabelShown),
button.boxed
]