如何使用自动布局在一个视图被隐藏时移动其他视图?

366

我已经在IB中设计了自定义单元格,并将其子类化,并将我的输出连接到我的自定义类。我在单元格内容中有三个子视图,它们是:UIView(cdView)和两个标签(titleLabel和emailLabel)。根据每行可用的数据,有时我希望在我的单元格中显示UIView和两个标签,有时只显示两个标签。我尝试设置约束,以便如果我将UIView属性设置为隐藏或从superview中删除它,那么这两个标签将向左移动。我尝试将UIView的前导约束设置为Superview(Cell content)10px,UILabels的前导约束设置为下一个视图(UIView)10px。稍后在我的代码中。

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(IndexPath *)indexPath {
    
    // ...

    Record *record = [self.records objectAtIndex:indexPath.row];
    
    if ([record.imageURL is equalToString:@""]) {
         cell.cdView.hidden = YES;
    }
}

我正在隐藏我的cell.cdView,希望标签移到左边,但它们仍然停留在Cell中相同的位置。我尝试将cell.cdView从superview中删除,但也没有起作用。我附加了一张图片以澄清我的意思。

cell

我知道如何通过编程实现这一点,但我不想要那个解决方案。我想要的是在IB中设置约束,并期望我的子视图可以根据其他视图的删除或隐藏而动态移动。是否可能使用自动布局在IB中实现这一点?

.....

更改约束值运行时 - 检查此答案 - Kampai
2
对于这种特定情况,您也可以使用UIStackView。当您隐藏cd时,标签将占据它们的空间。 - Marco Pappalardo
@MarcoPappalardo 这似乎是唯一正确的解决方案。 - Koen
24个回答

395

这是可能的,但您需要额外做一些工作。首先需要理解以下概念:

  • 即使它们不绘制,隐藏的视图仍参与自动布局,并通常保留其框架,使其他相关视图保持原位。
  • 从其超级视图中删除视图时,将从该视图层次结构中删除所有相关约束

在您的情况下,这可能意味着:

  • 如果您将左侧视图设置为隐藏,则标签将保持原位,因为该左侧视图仍占用空间(即使它不可见)。
  • 如果您删除左侧视图,则标签可能会被留下不明确的约束,因为您不再具有标签左侧边缘的约束。

您需要做的是明智地过度约束您的标签。保留现有约束(10个点的间距到另一个视图),但添加另一个约束:使标签的左侧边缘与其父视图的左侧边缘相距10个点,优先级为非必需(默认高优先级可能有效)。

然后,当您想要将它们向左移动时,请完全删除左侧视图。与其相关的强制性10个点约束将随着它所关联的视图一起消失,您将只剩下一个高优先级的约束,即标签与其父视图相隔10个点。在下一次布局通行证中,这应该会导致它们向左扩展,直到填满超级视图的宽度但留出边缘间距。

一个重要的警告:如果您想让左侧视图再次出现,则不仅必须将其添加回视图层次结构中,而且还必须同时重新建立所有其约束。这意味着您需要一种方法,在该视图再次显示时将其与标签之间的10pt间距约束恢复。


8
虽然这个答案肯定有效,但在我看来,为了处理不同的用例而过度约束似乎是一种代码异味——特别是因为你必须重新建立所有的约束条件,如果你想再次显示视图,就需要重新建立这些约束条件。 - memmons
32
我尊重地不同意。如果您将视图的宽度设置为0,那么就会遇到两个问题。首先,超级视图和可见视图之间现在有双倍的间距:|-(space)-[hidden(0)]-(space)-[visible] 实际上变成了 |-(2*space)-[visible]。其次,该视图可能会开始抛出约束冲突,具体取决于其自身的视图子树和约束条件- 您无法保证将视图任意约束为0宽度并使其继续工作。 - Tim
1
如果您正在使用具有内在内容大小的视图,则Tim的答案似乎是唯一的方法。 - balkoth
谢谢Tim。我设置了一个较高的优先级为0的约束条件,以避免约束不兼容,但现在我意识到有双倍间距的问题。我之前没有这个问题,因为我从未同时显示两个视图(我的情况是:|-[otherViews]-[eitherThis][orThis]-|),但我最终会遇到这个问题。 - Ferran Maylinch
只有使用优先级为250(低)时才有效。优先级为750(高)时无效。使用低优先级(如250)是否存在任何问题? - Crashalot
显示剩余4条评论

215

在运行时添加或删除约束是一项耗费资源的操作,可能会影响性能。不过,有一种更简单的替代方法。

对于您想要隐藏的视图,请设置一个宽度约束,并将其他视图与该视图之间的左边水平间距进行限制。

要隐藏该视图,请更新宽度约束的 .constant 值为 0.f。其他视图将自动向左移动以占据位置。

有关更多详细信息,请参见我在此处的其他答案:

如何在运行时更改标签约束?


32
这种解决方案唯一的问题是左边的边距会是你想要的两倍,所以我建议你也更新其中一个限制条件,但即使这样,我认为这比删除子视图要少做一些工作。 - José Manuel Sánchez
2
@skinsfan00atg 如果您正在使用具有内在内容大小的视图,则无法使用此解决方案。 - balkoth
2
如果您降低内容紧缩优先级,则不使用内在内容大小,而是使用约束指示的大小。 - balkoth
3
好的,我理解您的意思。当您想隐藏视图时,需要在代码中将压缩阻力优先级设置为0(而不是内容紧贴),并在需要再次显示时恢复该值。除此之外,您需要在界面构建器中添加一个约束来将视图的大小设置为0。不需要在代码中更改此约束。 - balkoth
1
一个小提示,NSLayoutConstraint 的常量类型是 CGFloat,它是 double 的一个 typedef(在 Apple Watch 上除外,那里是 float)。因此,为了避免混乱的强制转换,最好将宽度约束设置为 0.0 而不是 0.f - RobP
显示剩余6条评论

97

对于仅支持 iOS 8+ 的人,有一个新的布尔属性 active。它将帮助动态地启用仅所需的约束。

P.S. 约束点必须是强引用(strong),而不是弱引用(weak)。

示例:

@IBOutlet weak var optionalView: UIView!
@IBOutlet var viewIsVisibleConstraint: NSLayoutConstraint!
@IBOutlet var viewIsHiddenConstraint: NSLayoutConstraint!

func showView() {
    optionalView.isHidden = false
    viewIsVisibleConstraint.isActive = true
    viewIsHiddenConstraint.isActive = false
}

func hideView() {
    optionalView.isHidden = true
    viewIsVisibleConstraint.isActive = false
    viewIsHiddenConstraint.isActive = true
}

另外,要修复Storyboard中的错误,您需要取消其中一个约束条件的已安装复选框。

UIStackView (iOS 9+)
另一种选择是将您的视图包装在UIStackView中。一旦视图被隐藏,UIStackView会自动更新布局。


部分激活有效。但如果我将其用于可重复使用的单元格中,从激活到停用都有效,但从停用到激活则无效。有什么想法吗?或者您可以提供一个示例吗? - Aoke Li
10
这种情况的发生是因为未加载或释放了已取消激活的约束。你的约束输出应该是 strong(强引用),而不是 weak(弱引用)。 - Silmaril
我们在哪里可以找到“active”属性? - Lakshmi Reddy
https://developer.apple.com/library/ios/documentation/AppKit/Reference/NSLayoutConstraint_Class/#//apple_ref/occ/instp/NSLayoutConstraint/active - Silmaril
2
似乎过度约束和设置约束活动可能是最合理的答案。 - Ting Yi Shih
显示剩余5条评论

90

UIStackView在任一子视图的hidden属性被更改时会自动重新定位其视图位置(iOS 9+)。

UIView.animateWithDuration(1.0) { () -> Void in
   self.mySubview.hidden = !self.mySubview.hidden
}

点击此WWDC视频中的11:48以查看演示:

自动布局之谜,第1部分


我隐藏了一个嵌套的堆栈视图,整个包含的堆栈视图都消失了。 - Ian Warburton
7
应该采纳这个答案。 遵循苹果在2015年举行的WWDC上关于Interface Builder设计的建议。 - thibaut noah
@thibautnoah 那么iOS 8的支持呢? - bagage
7
除非你是Facebook或Google,否则你可以放弃它。 覆盖iOS 9以上的设备将支持超过90%的设备,足够了。 支持以下设备将瘫痪您的开发流程,并防止您使用最新功能。 - thibaut noah
1
这是现在的正确答案。所有其他答案都已过时。 - Ethan Allen

18

我的项目使用一个自定义的@IBDesignable子类,继承自UILabel(以确保颜色、字体、插图等的一致性),我实现了类似以下的内容:

override func intrinsicContentSize() -> CGSize {
    if hidden {
        return CGSizeZero
    } else {
        return super.intrinsicContentSize()
    }
}

这使标签子类可以参与自动布局,但在隐藏时不占用空间。


12

针对谷歌员工: 在Max的回答基础上,为了解决许多人注意到的填充问题,我简单地增加了标签的高度,并将该高度用作分隔符,而不是实际的填充。这个想法可以扩展到任何包含视图的情况。

这里有一个简单的例子:

IB Screenshot

在这种情况下,我将作者标签的高度映射到一个适当的IBOutlet中:

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

当我将约束的高度设置为0.0f时,我们保留“填充”,因为播放按钮的高度可以容纳它。


对于那些不熟悉NSLayoutConstraint的人,我相信你想要更新authorLabelHeightconstant属性 - Kyle

9

把UI视图和标签之间的连接设为IBOutlet,并且在hidden=YES时将优先级成员设置为一个较小的值。


3
一旦 NSLayoutConstraint 被建立,就无法调整其优先级。你需要删除并重新添加一个具有不同优先级的新约束来实现调整。 - Tim
7
我已经成功运用了这个方法。我有一个使用标签和按钮的案例,在其中我需要隐藏按钮并使标签扩展。我有两个约束条件,一个最初优先级为751,另一个为750。然后当我隐藏按钮时,我翻转这些优先级,标签的长度会增加。需要注意的是,如果你尝试将更高的优先级设为1000,则会出现错误“Mutating a priority from required to not on an installed constraint (or vice-versa) is not supported.”,所以不要这样做,你看起来是没问题的。Xcode 5.1/viewDidLoad。 - John Bushnell

8
我的做法是创建了两个xib文件,一个带有左视图,另一个则没有。我在控制器中注册了这两个文件,并在cellForRowAtIndexPath方法中决定使用哪一个。
它们使用相同的UITableViewCell类。缺点是xib文件之间的内容有些重复,但是这些单元格非常基本。好处是我不需要大量代码手动管理删除视图、更新约束等等。
一般来说,这可能是更好的解决方案,因为它们在技术上是不同的布局,因此应该有不同的xib文件。
[self.table registerNib:[UINib nibWithNibName:@"TrackCell" bundle:nil] forCellReuseIdentifier:@"TrackCell"];
[self.table registerNib:[UINib nibWithNibName:@"TrackCellNoImage" bundle:nil] forCellReuseIdentifier:@"TrackCellNoImage"];

TrackCell *cell = [tableView dequeueReusableCellWithIdentifier:(appDelegate.showImages ? @"TrackCell" : @"TrackCellNoImage") forIndexPath:indexPath];

8
只需使用UIStackView,一切都会正常工作。不需要担心其他约束,UIStackView会自动处理空间。

我肯定会被你建议的嵌套StackViews解决这个问题所吸引。 - clearlight

7
在这种情况下,我将“作者”标签的高度映射到一个适当的IBOutlet。
@property (retain, nonatomic) IBOutlet NSLayoutConstraint* authorLabelHeight;

当我将约束的高度设置为0.0f时,我们保留了“填充”,因为播放按钮的高度允许它存在。

cell.authorLabelHeight.constant = 0;

enter image description here enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接