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

Posted

技术标签:

【中文标题】隐藏视图时如何使用自动布局移动其他视图?【英文标题】:How to use Auto Layout to move other views when a view is hidden? 【发布时间】:2013-08-06 14:51:32 【问题描述】:

我在 IB 中设计了我的自定义单元,将其子类化并将我的插座连接到我的自定义类。我在单元格内容中有三个子视图,它们是:UIView (cdView) 和两个标签(titleLabel 和 emailLabel)。根据每行可用的数据,有时我希望在单元格中显示 UIView 和两个标签,有时只显示两个标签。我要做的是设置约束,如果我将 UIView 属性设置为隐藏,或者我将从超级视图中删除它,这两个标签将向左移动。我尝试将 UIView 前导约束设置为 10 像素的 Superview(单元格内容)和 10 像素的 UILabel 前导约束到下一个视图(UIView)。稍后在我的代码中

-(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 中的位置保持不变。我试图从 superview 中删除 cell.cdView 但它也不起作用。我附上了图片以阐明我的意思。

我知道如何以编程方式执行此操作,但我不是在寻找那个解决方案。我想要的是在 IB 中设置约束,我希望如果其他视图被删除或隐藏,我的子视图将动态移动。是否可以通过自动布局在 IB 中执行此操作?

.....

【问题讨论】:

更改约束值运行时 - 检查此answer 对于这种特殊情况,您还可以使用 UIStackView。当你隐藏 cd 时,标签会占据它们的空间 @MarcoPappalardo 这确实似乎是唯一正确的解决方案 【参考方案1】:

这是可能的,但您必须做一些额外的工作。首先有几个概念性的事情要解决:

隐藏的视图,即使它们不绘制,仍然参与自动布局,并且通常保留它们的框架,而将其他相关视图留在它们的位置。 从父视图中删除视图时,所有相关约束也会从该视图层次结构中删除。

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

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

您需要做的是明智地过度限制您的标签。单独保留现有的约束(另一个视图的 10pts 空间),但添加另一个约束:使标签的左边缘距其父视图的左边缘 10pts,并具有非必需的优先级(默认的高优先级可能会很好用)。

然后,当您希望它们向左移动时,请完全移除左侧视图。对左视图的强制 10pt 约束将与它相关的视图一起消失,并且您将只剩下一个高优先级约束,即标签距离其父视图 10pts。在下一个布局过程中,这应该会导致它们向左扩展,直到它们填满超级视图的宽度,但要在边缘周围留出间距。

一个重要的警告:如果您希望您的左视图重新出现在图片中,您不仅必须将其重新添加到视图层次结构中,而且您还必须重新建立其所有约束同时。这意味着每当再次显示该视图时,您需要一种方法将视图及其标签之间的 10pt 间距约束放回原处。

【讨论】:

虽然这个答案肯定会奏效,但 IMO,处理各种用例的过度约束似乎是一种代码异味——特别是因为您必须为您想要的任何已删除视图重新建立所有约束再次显示视图。 在我看来,这不是要走的路。您应该对要隐藏的视图使用宽度/高度约束。 我不同意。如果您(例如)将视图的宽度设置为 0,您将遇到两个问题。首先,现在超级视图和可见视图之间有双倍间距:|-(space)-[hidden(0)]-(space)-[visible] 实际上是|-(2*space)-[visible]。其次,该视图可能会根据其自己的视图子树和约束开始引发约束违规——您不能保证可以任意将视图约束为 0 宽度并使其继续工作。 如果您使用的是具有内在内容大小的视图,蒂姆的答案似乎是独特的方式。 谢谢蒂姆。我设置了一个更高优先级的约束 0 以避免约束不兼容,但现在我意识到双倍间距存在问题。我没有这个问题,因为我从来没有同时显示两个视图(我的案例:|-[otherViews]-[eitherThis][orThis]-|),但我最终会遇到这个问题。【参考方案2】:

在运行时添加或删除约束是一项可能影响性能的重量级操作。但是,还有一个更简单的选择。

对于您要隐藏的视图,设置宽度约束。将其他视图与该视图的前导水平间隙限制在一起。

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

查看我的其他答案了解更多详情:

How to change label constraints during runtime?

【讨论】:

此解决方案的唯一问题是左边的边距将是您可能想要的两倍,所以我也会更新其中一个约束,但即便如此我认为这不是工作而不是删除子视图。 @skinsfan00atg 如果您使用的是具有固有内容大小的视图,则不能使用此解决方案。 @MaxMacLeod 如果降低内容拥抱优先级,那么您没有使用内在内容大小,而是使用约束指示的大小。 @MaxMacLeod 好的,我明白你的意思了。当您想要隐藏视图时,您需要在代码中将压缩阻力优先级设置为 0(而不是内容拥抱),并且当您想要再次显示时恢复该值。除此之外,您还需要在界面生成器中添加一个约束来将视图的大小设置为 0。代码中不需要触及这个约束。 一个小注意,NSLayoutConstraint 的常量是CGFloat 类型,它是double 的类型定义(Apple Watch 除外,它是float)。所以为了避免混乱的演员表,最好将宽度约束设置为0.0而不是0.f【参考方案3】:

对于仅支持 iOS 8+ 的用户,有一个新的布尔属性 active。这将有助于动态启用仅需要的约束

附:约束出口必须是,而不是弱 示例:

@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

此外,要修复情节提要中的错误,您还需要取消选中 Installed 复选框以获取这些约束之一。 UIStackView ios 9+) 另一种选择是将您的视图包装在UIStackView 中。一旦视图被隐藏UIStackView 将自动更新布局

【讨论】:

活动部分有效。但是,如果我在可重复使用的单元格中使用它,从激活到停用,可以工作,但不能用于停用激活。有任何想法吗?或者你能举个例子吗? 发生这种情况是因为未加载或释放已停用的约束。你的约束出口应该是强的,而不是弱的 我们在哪里可以找到“活动”属性? developer.apple.com/library/ios/documentation/AppKit/Reference/… 似乎过度约束+设置约束活动可能是最合理的答案。【参考方案4】:

UIStackView 在其任何子视图 (iOS 9+) 上的 hidden 属性发生更改时会自动重新定位其视图。

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

在这个 WWDC 视频中跳转到 11:48 观看演示:

Mysteries of Auto Layout, Part 1

【讨论】:

我隐藏了一个嵌套的堆栈视图,整个包含的堆栈视图消失了。 这应该是公认的答案。遵循苹果在 wwdc 2015 上对界面构建器设计的建议。 @thibautnoah 那么 iOS 8 支持呢? @bagage 除非你是 facebook 或 google,否则你可以放弃它。在 iOS 9 之后的覆盖范围内,您将支持 90% 以上的设备,绰绰有余。以下支持将削弱您的开发过程并阻止您使用 imo 的最新功能 这是正确的答案。所有其他答案都已过时。【参考方案5】:

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

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

这允许标签子类参与自动布局,但隐藏时不占用空间。

【讨论】:

【参考方案6】:

对于 Google 员工: 基于 Max 的回答,为了解决许多人注意到的填充问题,我只是增加了标签的高度并将该高度用作分隔符而不是实际的填充。这个想法可以扩展到任何包含视图的场景。

这是一个简单的例子:

在这种情况下,我将 Author 标签的高度映射到适当的IBOutlet

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

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

【讨论】:

对于NSLayoutConstraint 的新手,我相信您想更新constant property 的authorLabelHeight【参考方案7】:

将uiview和标签之间的约束连接为IBOutlet,并在设置hidden = YES时将优先级成员设置为较小的值

【讨论】:

一个NSLayoutConstraint一旦建立就不能调整它的优先级;您必须删除并读取具有不同优先级的新约束。 我已经让这个方法起作用了。我有一个使用标签和按钮的案例,我需要隐藏按钮并展开标签。我有两个约束,一个最初具有优先级 751,另一个具有 750。然后当我隐藏按钮时,我翻转优先级,标签的长度会增加。应该注意的是,如果您尝试将较高的优先级设为 1000,则会收到错误消息“不支持将优先级从必需更改为不安装约束(反之亦然)。”。所以不要,你似乎很好。 Xcode 5.1/viewDidLoad.【参考方案8】:

我最终做的是创建 2 个 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];

【讨论】:

【参考方案9】:

在这种情况下,我将 Author 标签的高度映射到适当的 IBOutlet:

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

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

cell.authorLabelHeight.constant = 0;

【讨论】:

【参考方案10】:

使用两个 UIStackView 水平和垂直,当堆栈中的某些子视图视图被隐藏时,其他堆栈子视图将被移动,使用分布 -> 为垂直堆栈按比例填充两个 UILabel 并且需要为第一个 UIView 设置宽度和高度约束

【讨论】:

【参考方案11】:

只需使用 UIStackView,一切都会好起来的。 不用担心其他约束,UIStackView 会自动处理空间。

【讨论】:

【参考方案12】:

对于这个特定的布局,要使用的约束是隐藏视图上的“前导”约束。不过,下面的理论将适用于所有方向。

1:设置所有约束在所有视图可见时的外观。

2:向要隐藏的视图添加第二个“前导”约束。这将暂时打破限制。

3:将原始前导约束的优先级更改为 '999' - 然后将优先级设置为 1000 的新约束,并且不会再破坏任何约束。

4:将新约束从“leading=leading”更改为“trailing=leading”。这会将您要隐藏的视图从其父级的前沿移开。

5:切换新约束的 isActive 值现在将切换它是否在视图中或视图之外。在将可见性设置为真/假的同时将其设置为真/假。例如:

@IBOutlet var avatar:UIImage!
@IBOutlet var avatarLeadHid:NSLayoutConstraint!

func hideAvatar() 
  self.avatar.isHidden = true
  self.avatarLeadHid.isActive = true


func showAvatar() 
  self.avatar.isHidden = false
  self.avatarLeadHid.isActive = false

奖励:您可以调整新隐藏约束的“常量”值,以更改隐藏视图时使用的填充/边距。这个值可以是负数。

额外奖励:只需切换 hider-constraint 上的“已安装”复选框,即可在 Interface Builder 中查看布局的外观,而无需运行任何代码。

进一步的帮助:我制作了一个视频,展示了我在哪些方面做得比分数列表更好:https://youtu.be/3tGEwqtQ-iU

【讨论】:

【参考方案13】:

在我的例子中,我将高度约束的常量设置为0.0f,并将hidden属性设置为YES

为了再次显示视图(带有子视图),我做了相反的事情:我将高度常数设置为非零值并将hidden 属性设置为NO

【讨论】:

【参考方案14】:

试试这个,我已经实现了下面的代码,

我在 ViewController 上有一个视图,其中添加了其他三个视图,当隐藏任何视图时,其他两个视图会移动,请按照以下步骤操作。 ,

1.ViewController.h 文件

#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property (strong, nonatomic) IBOutlet UIView *viewOne;
@property (strong, nonatomic) IBOutlet UIView *viewTwo;
@property (strong, nonatomic) IBOutlet UIView *viewThree;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewOneWidth;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewTwoWidth;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewThreeWidth;
@property (strong, nonatomic) IBOutlet NSLayoutConstraint *viewBottomWidth;
@end

2.ViewController.m

 #import "ViewController.h"
 @interface ViewController ()

  CGFloat viewOneWidthConstant;
  CGFloat viewTwoWidthConstant;
  CGFloat viewThreeWidthConstant;
  CGFloat viewBottomWidthConstant;

@end

@implementation ViewController
@synthesize viewOne, viewTwo, viewThree;

- (void)viewDidLoad 
  [super viewDidLoad];
 // Do any additional setup after loading the view, typically from a 
  nib.

  /*
   0  0   0
   0  0   1
   0  1   0
   0  1   1
   1  0   0
   1  0   1
   1  1   0
   1  1   1
   */

  //    [viewOne setHidden:NO];
  //    [viewTwo setHidden:NO];
  //    [viewThree setHidden:NO];

  //    [viewOne setHidden:NO];
  //    [viewTwo setHidden:NO];
  //    [viewThree setHidden:YES];

  //    [viewOne setHidden:NO];
  //    [viewTwo setHidden:YES];
  //    [viewThree setHidden:NO];

  //    [viewOne setHidden:NO];
  //    [viewTwo setHidden:YES];
  //    [viewThree setHidden:YES];


  //    [viewOne setHidden:YES];
  //    [viewTwo setHidden:NO];
  //    [viewThree setHidden:NO];

  //    [viewOne setHidden:YES];
  //    [viewTwo setHidden:NO];
  //    [viewThree setHidden:YES];

 //    [viewOne setHidden:YES];
 //    [viewTwo setHidden:YES];
 //    [viewThree setHidden:NO];

//    [viewOne setHidden:YES];
//    [viewTwo setHidden:YES];
//    [viewThree setHidden:YES];

 [self hideShowBottomBar];
  

- (void)hideShowBottomBar

  BOOL isOne = !viewOne.isHidden;
  BOOL isTwo = !viewTwo.isHidden;
  BOOL isThree = !viewThree.isHidden;

  viewOneWidthConstant = _viewOneWidth.constant;
  viewTwoWidthConstant = _viewTwoWidth.constant;
  viewThreeWidthConstant = _viewThreeWidth.constant;
  viewBottomWidthConstant = _viewBottomWidth.constant;

   if (isOne && isTwo && isThree) 
    // 0    0   0
    _viewOneWidth.constant = viewBottomWidthConstant / 3;
    _viewTwoWidth.constant = viewBottomWidthConstant / 3;
    _viewThreeWidth.constant = viewBottomWidthConstant / 3;
    
    else if (isOne && isTwo && !isThree) 
     // 0    0   1
    _viewOneWidth.constant = viewBottomWidthConstant / 2;
    _viewTwoWidth.constant = viewBottomWidthConstant / 2;
    _viewThreeWidth.constant = 0;
    
   else if (isOne && !isTwo && isThree) 
    // 0    1   0
    _viewOneWidth.constant = viewBottomWidthConstant / 2;
    _viewTwoWidth.constant = 0;
    _viewThreeWidth.constant = viewBottomWidthConstant / 2;
    
    else if (isOne && !isTwo && !isThree) 
    // 0    1   1
    _viewOneWidth.constant = viewBottomWidthConstant;
    _viewTwoWidth.constant = 0;
    _viewThreeWidth.constant = 0;
   
   else if (!isOne && isTwo && isThree) 
    // 1    0   0
    _viewOneWidth.constant = 0;
    _viewTwoWidth.constant = viewBottomWidthConstant / 2;
    _viewThreeWidth.constant = viewBottomWidthConstant / 2;
   
   else if (!isOne && isTwo && !isThree) 
    // 1    0   1
    _viewOneWidth.constant = 0;
    _viewTwoWidth.constant = viewBottomWidthConstant;
    _viewThreeWidth.constant = 0;
   
   else if (!isOne && !isTwo && isThree) 
    // 1    1   0
    _viewOneWidth.constant = 0;
    _viewTwoWidth.constant = 0;
    _viewThreeWidth.constant = viewBottomWidthConstant;
   
   else if (isOne && isTwo && isThree) 
    // 1    1   1
    _viewOneWidth.constant = 0;
    _viewTwoWidth.constant = 0;
    _viewThreeWidth.constant = 0;
   
  

 - (void)didReceiveMemoryWarning 
  [super didReceiveMemoryWarning];
 // Dispose of any resources that can be recreated.
 
 @end

希望所以这个逻辑会帮助一些人。

【讨论】:

【参考方案15】:

我将使用水平堆栈视图。它可以在子视图隐藏时移除框架。

在下图中,红色视图是您的内容的实际容器,并且与橙色超级视图 (ShowHideView) 有 10pt 的尾随空间,然后只需将 ShowHideView 连接到 IBOutlet 并以编程方式显示/隐藏/删除它。

    此时视图可见/已安装。

    此时视图处于隐藏/未安装状态。

【讨论】:

【参考方案16】:

这是我使用优先级约束的另一个解决方案。这个想法是把宽度设置为0。

    创建容器视图(橙色)并设置宽度。

    创建内容视图(红色)并将尾随空格 10pt 设置为超级视图(橙色)。注意尾随空间约束,有 2 个具有不同优先级的尾随约束。低(=10)和高(

    将橙色视图的宽度设置为 0 以隐藏视图。

【讨论】:

【参考方案17】:

最简单的解决方案是使用 UIStackView(水平)。添加到堆栈视图:第一个视图和带有标签的第二个视图。 然后将第一个视图的 isHidden 属性设置为 false。 所有约束都将被计算并自动更新。

【讨论】:

【参考方案18】:

正如 no_scene 建议的那样,您绝对可以通过在运行时更改约束的优先级来做到这一点。这对我来说要容易得多,因为我有多个必须删除的阻塞视图。

这是一个使用 ReactiveCocoa 的 sn-p:

RACSignal* isViewOneHiddenSignal = RACObserve(self.viewModel, isViewOneHidden);
RACSignal* isViewTwoHiddenSignal = RACObserve(self.viewModel, isViewTwoHidden);
RACSignal* isViewThreeHiddenSignal = RACObserve(self.viewModel, isViewThreeHidden);
RAC(self.viewOne, hidden) = isViewOneHiddenSignal;
RAC(self.viewTwo, hidden) = isViewTwoHiddenSignal;
RAC(self.viewThree, hidden) = isViewThreeHiddenSignal;

RAC(self.viewFourBottomConstraint, priority) = [[[[RACSignal
    combineLatest:@[isViewOneHiddenSignal,
                    isViewTwoHiddenSignal,
                    isViewThreeHiddenSignal]]
    and]
    distinctUntilChanged]
    map:^id(NSNumber* allAreHidden) 
        return [allAreHidden boolValue] ? @(780) : @(UILayoutPriorityDefaultHigh);
    ];

RACSignal* updateFramesSignal = [RACObserve(self.viewFourBottomConstraint, priority) distinctUntilChanged];
[updateFramesSignal
    subscribeNext:^(id x) 
        @strongify(self);
        [self.view setNeedsUpdateConstraints];
        [UIView animateWithDuration:0.3 animations:^
            [self.view layoutIfNeeded];
        ];
    ];

【讨论】:

【参考方案19】:

如果这对某人有帮助,我构建了一个帮助类来使用visual format 约束。我正在我当前的应用程序中使用它。

AutolayoutHelper

它可能有点适合我的需要,但您可能会发现它很有用,或者您可能想要修改它并创建自己的助手。

我要感谢蒂姆的answer above、answer about UIScrollView 和tutorial。

【讨论】:

【参考方案20】:

以下是我将如何重新调整我的 uiviews 以获得您的解决方案:

    将一个 UIImageView 拖放到左侧。 拖放一个 UIView 并将其放置在 UIImageView 的右侧。 将两个 UILabel 拖放到该 UIView 中,其前导和尾随约束均为零。 将包含 2 个标签的 UIView 的前导约束设置为 superview 而不是 UIImagView。 如果 UIImageView 被隐藏,则将前导约束常量设置为 10 px 以进行超级视图。否则,将前导约束常量设置为 10 px + UIImageView.width + 10 px。

我创建了自己的拇指规则。每当您必须隐藏/显示其约束可能受到影响的任何 uiview 时,请在 uiview 中添加所有受影响/依赖的子视图,并以编程方式更新其前导/尾随/顶部/底部约束常量。

【讨论】:

【参考方案21】:

这是一个老问题,但我仍然希望它会有所帮助。来自 android,在这个平台上,您有一个方便的方法 isVisible 将其从视图中隐藏,但在自动布局绘制视图时也不会考虑框架。

使用扩展和“扩展”uiview,您可以在 ios 中执行类似的功能(不知道为什么它已经不在 UIKit 中)这里是 swift 3 中的实现:

    func isVisible(_ isVisible: Bool) 
        self.isHidden = !isVisible
        self.translatesAutoresizingMaskIntoConstraints = isVisible
        if isVisible  //if visible we remove the hight constraint 
            if let constraint = (self.constraints.filter$0.firstAttribute == .height.first)
                self.removeConstraint(constraint)
            
         else  //if not visible we add a constraint to force the view to have a hight set to 0
            let height = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal , toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: 0)
            self.addConstraint(height)
        
        self.layoutIfNeeded()
    

【讨论】:

【参考方案22】:

正确的做法是使用 isActive = false 禁用约束。但是请注意,停用约束会删除并释放它,因此您必须为它们提供强大的出口。

【讨论】:

【参考方案23】:

我认为这是最简单的答案。请验证它是否有效:

        StackFullView.layer.isHidden = true
        Task_TopSpaceSections.constant = 0.   //your constraint of top view

在这里查看https://www.youtube.com/watch?v=EBulMWMoFuw

【讨论】:

【参考方案24】:

当您想要隐藏 UIView 时,创建 宽度约束 并在代码中将其更改为 0,而不是隐藏视图。

这可能是最简单的方法。此外,它将保留视图,如果您想再次显示它,则无需重新创建它(非常适合在表格单元格内使用)。要更改常量值,您需要创建一个常量引用插座(与您为视图创建插座的方式相同)。

【讨论】:

以上是关于隐藏视图时如何使用自动布局移动其他视图?的主要内容,如果未能解决你的问题,请参考以下文章

使用带有自动布局的推送时隐藏按钮时,约束更新导致项目移动

如何移动子视图控制器,同时使用自动布局而不是框架隐藏状态栏?

使用自动布局隐藏视图并重新定位其他视图

自动布局:如何隐藏包含子视图的 UIView?

xcode自动布局隐藏视图占用空间

在自动布局中方向更改为横向时隐藏视图