【问题标题】:AutoLayout with hidden UIViews?带有隐藏 UIView 的自动布局?
【发布时间】:2013-11-02 21:14:40
【问题描述】:

我觉得根据业务逻辑显示/隐藏UIViews(最常见的是UILabels)是一种相当常见的范例。我的问题是,使用 AutoLayout 响应隐藏视图的最佳方式是什么,就好像它们的框架是 0x0 一样。以下是 1-3 个功能的动态列表示例。

现在我从按钮到最后一个标签有一个 10px 的顶部空间,当标签隐藏时显然不会向上滑动。截至目前,我为这个约束创建了一个出口,并根据我显示的标签数量来修改常量。这显然有点棘手,因为我使用负常数值将按钮向上推到隐藏帧上。这也很糟糕,因为它不受实际布局元素的限制,只是基于其他元素的已知高度/填充的偷偷摸摸的静态计算,并且显然与 AutoLayout 的构建目的作斗争。

显然,我可以根据我的动态标签创建新的约束,但是对于试图折叠一些空白来说,这是大量的微观管理和大量的冗长。有更好的方法吗?更改帧大小 0,0 并让 AutoLayout 在不操纵约束的情况下完成其工作?完全删除视图?

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

2018 年 2 月编辑:查看 Ben 的回答,UIStackViews

【问题讨论】:

  • 感谢 Ryan 提出这个问题。正如你所问的那样,我正在发疯如何处理这些案件。每次我查看自动布局的教程时,他们中的大多数人都说参考 raywenderlich 教程网站,我发现它有点难以理解。

标签: ios objective-c autolayout


【解决方案1】:

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

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

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

无需添加或删除约束,这是一项重量级操作。只需更新约束的 constant 即可。

【讨论】:

  • 完全正确。只要 - 在本例中 - 您使用水平间隙将视图 B 定位到视图 A 的右侧。我一直使用这种技术,效果很好
  • @MaxMacLeod 只是为了确保:如果两个视图之间的间隙为 x,为了使视图 B 从视图 A 位置开始,我们也必须将间隙约束常量更改为 0对吧?
  • @kernix 是的,如果需要在两个视图之间设置水平间隙约束。常数 0 当然意味着没有间隙。因此,通过将视图 A 的宽度设置为 0 来隐藏视图 A 会将视图 B 向左移动以与容器齐平。顺便说一句,感谢您的支持!
  • @MaxMacLeod 感谢您的回答。我尝试使用包含其他视图的 UIView 执行此操作,但是当我将其高度约束设置为 0 时,该视图的子视图并未隐藏。此解决方案是否应该与包含子视图的视图一起使用?谢谢!
  • 也希望有一个适用于包含其他视图的 UIView 的解决方案...
【解决方案2】:

在隐藏时使用常量0 并在再次显示时使用另一个常量的解决方案是有效的,但如果您的内容具有灵活的大小,则令人不满意。您需要衡量您的灵活内容并设置一个常数。这感觉不对,如果内容因服务器或 UI 事件而改变大小,就会出现问题。

我有更好的解决方案。

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

这是你的做法:

1.在低优先级的界面生成器中设置宽度(或高度)为 0。

界面生成器不会因为优先级低而大喊大叫。通过暂时将优先级设置为 999 来测试高度行为(1000 禁止以编程方式变异,因此我们不会使用它)。界面构建器现在可能会大喊冲突的约束。您可以通过将相关对象的优先级设置为 900 左右来解决这些问题。

2。添加一个插座,以便您可以在代码中修改宽度约束的优先级:

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

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

【讨论】:

  • @SimplGy 你能告诉我这个地方是什么吗?即将关闭?
  • 这不是一般解决方案的一部分,@Niharika,这正是我的业务规则(如果餐厅正在/不会很快关闭,请显示视图)。
【解决方案3】:

UIStackView 可能是适用于 iOS 9+ 的方式。它不仅可以处理隐藏视图,如果设置正确,它还会移除额外的间距和边距。

【讨论】:

  • 在 UITableViewCell 中使用 UIStackView 时要小心。对我来说,一旦视图变得非常复杂,滚动就开始变得不稳定,每次滚动进/出单元格时它都会结结巴巴。在幕后,StackView 正在添加/删除约束,这对于平滑滚动不一定足够有效。
  • 最好举一个小例子来说明堆栈视图如何处理这个问题。
  • @Kento 这在 iOS 10 上仍然是一个问题吗?
  • 五年后...我应该更新并将其设置为接受的答案吗?现已推出三个发布周期。
  • @RyanRomanchuk 你应该,这绝对是现在最好的答案。谢谢本!
【解决方案4】:

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

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

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

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

【讨论】:

    【解决方案5】:

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

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

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

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

    constraintB.priority = .defaultHigh

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

    constraintB.priority = .defaultLow

    与仅更改约束 B 的 isActive 相比,此方法的(不可否认的次要)优势是,如果通过其他方式将瞬态元素从视图中删除,您仍然有一个有效的约束。

    【讨论】:

    • 这具有在情节提要和代码(例如元素高度/宽度)中没有重复值的优点。非常优雅。
    • 谢谢!!它帮助了我
    • 好方法,谢谢
    【解决方案6】:

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

    【讨论】:

    • 这很棒。不过,某些 UI 元素需要触发 invalidateIntrinsicContentSize。您可以在覆盖的 setHidden 中执行此操作。
    【解决方案7】:

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

    知道这个事实/错误可以帮助一些人简化他们的布局,甚至可能是原始问题的布局,所以我想我会发布它。

    【讨论】:

    • 我认为你不需要隐藏它。将其文本设置为空字符串就足够了。
    【解决方案8】:

    我很惊讶UIKit 没有为这种期望的行为提供更优雅的方法。想要能够做到这一点似乎是一件很平常的事情。

    由于将约束连接到 IBOutlets 并将它们的常量设置为 0 感觉很糟糕(并且当您的视图有子视图时导致 NSLayoutConstraint 警告),我决定创建一个扩展来提供 simple, stateful approach to hiding/showing a UIView that has Auto Layout constraints

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

    编辑 此答案针对 iOS 8.4 及更低版本。在 iOS 9 中,只需使用 UIStackView 方法。

    【讨论】:

    • 这也是我的首选方法。这是对此处其他答案的重大改进,因为它不仅将视图压缩为零大小。除了微不足道的支出外,壁球方法会带来任何问题。这方面的一个例子是隐藏项目在this other answer 中的大间隙。如前所述,您最终可能会违反约束。
    • @DanielSchlaug 感谢您指出这一点。我已经更新了 MIT 的许可证。但是,每次有人实施此解决方案时,我都需要精神上的高五。
    【解决方案9】:

    最佳实践是,一旦所有内容都具有正确的布局约束,添加高度或带约束,这取决于您希望周围视图如何移动并将约束连接到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,但是,您不应忘记再次将其恢复为最高优先级。

    【讨论】:

    • 那是因为约束和视图不能总是被视图层次结构保留,所以如果你停用约束并隐藏视图,你仍然会保留它们以再次显示它们,无论你的视图层次结构决定是否保留。
    【解决方案10】:

    我建立类别来轻松更新约束:

    [myView1 hideByHeight:YES];
    

    在这里回答: Hide autolayout UIView : How to get existing NSLayoutConstraint to update this one

    【讨论】:

    • 不幸的是,在黄色箭头所指的地方,你得到了一个双倍间距。
    • 我认为与上述其他解决方案一样。一点都不理想。
    【解决方案11】:

    我的首选方法与 Jorge Arimany 建议的方法非常相似。

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

    然后创建另一个约束,指定隐藏第二个按钮时按钮和顶部标签之间的高度。创建第二个约束的出口并确保该出口具有强指针。然后在界面生成器中取消选中installed 复选框,并确保第一个约束的优先级低于第二个约束的优先级。

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

    就是这样,没有幻数,没有数学,如果您有多个要打开和关闭的约束,您甚至可以使用插座集合来按状态组织它们。 (AKA 将所有隐藏约束保留在一个 OutletCollection 中,将非隐藏约束保留在另一个中,并且只需遍历每个集合以切换它们的 .isActive 状态)。

    我知道 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()
        }
    }
    

    【讨论】:

      【解决方案12】:

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

      我的方法删除所有视图(在我的情况下为 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);;
      }];
      

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

      【讨论】:

        【解决方案13】:

        试试BoxView,它使动态布局简洁易读。
        你的情况是:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-08-25
          • 2014-04-04
          • 1970-01-01
          • 2014-04-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多