【问题标题】:Auto layout UIScrollView with subviews with dynamic heights具有动态高度的子视图的自动布局 UIScrollView
【发布时间】:2013-05-25 09:07:52
【问题描述】:

我在使用自动布局约束时遇到了 UIScrollView 的问题。 我有以下视图层次结构,通过 IB 设置约束:

- ScrollView (leading, trailing, bottom and top spaces to superview)
-- ContainerView (leading, trailing, bottom and top spaces to superview)
--- ViewA (full width, top of superview)
--- ViewB (full width, below ViewA)
--- Button (full width, below ViewB)

ViewA 和 ViewB 的初始高度为 200 点,但可以通过单击垂直扩展至 400 点的高度。 ViewA 和 ViewB 通过更新它们的高度约束(从 200 到 400)来扩展。这是对应的sn-p:

if(self.contentVisible) {
    heightConstraint.constant -= ContentHeight;
    // + additional View's internal constraints update to hide additional content 
    self.contentVisible = NO;
} else {
    heightConstraint.constant += ContentHeight;
    // + additional View's internal constraints update to show additional content
    self.contentVisible = YES;
}

[self.view setNeedsUpdateConstraints];
[UIView animateWithDuration:.25f animations:^{
    [self.view layoutIfNeeded];
}];

我的问题是,如果两个视图都展开,我需要能够滚动才能看到整个内容,而现在滚动不起作用。如何使用约束更新滚动视图以反映 ViewA 和 ViewB 高度的变化?

目前我能想到的唯一解决方案是在动画后手动设置ContainerView的高度,这将是ViewA + ViewB + Button的高度之和。但我相信有更好的解决方案?

谢谢

【问题讨论】:

标签: ios objective-c uiscrollview autolayout


【解决方案1】:

我使用如下纯结构

-view
  -scrollView
    -view A
    -view B
    -Button

确保 Button(THE LAST view) 有一个约束(从其底部到父视图的垂直间距,即滚动视图),在这种情况下,无论您的视图 A 和视图发生什么变化B 是,scrollView 的高度会相应改变。

我参考了这个很棒的在线book site

只需阅读“创建滚动视图”部分,您应该会有一个想法。

我在创建详细视图时遇到了类似的问题,并且使用具有自动布局的 Interface Builder 非常适合该任务!

祝你好运!

(其他资源:

关于auto layout for scroll view的堆栈溢出讨论。

iOS 6 有一个 Release Notes 谈论 UIScrollView 的自动布局支持。

关于滚动视图的免费在线iOS book explanation。这实际上对我帮助很大!

【讨论】:

  • 感谢您的资源!计划的小改动,我的客户想要 iOS 5 支持,所以我不得不关闭自动布局,但我稍后会回来。
  • "确保 Button(THE LAST view) 有一个约束(从其底部到父视图的垂直间距,即滚动视图)"
  • 不错的答案。我的问题是在 UIView 中有动态大小的标签,它是滚动视图的子视图。按照此处显示的结构,我将标签从 UIView 中取出,然后将其删除。我在标签上设置了约束,我的代码将滚动视图的内容大小设置为标签的文本大小,效果很好。
【解决方案2】:

假设我们有这样一个层次结构(Label1 是 ContentView 的子视图;ContentView 是 ScrollView 的子视图,ScrollView 是 viewcontroller 的视图的子视图):

ViewController's View ScrollView ContentView Label1 Label2 Label3

ScrollView 以正常方式被自动布局约束到 viewcontroller 的视图。

ContentView 被固定在滚动视图的顶部/左侧/右侧/底部。这意味着您有约束使 ContentView 的顶部/底部/前导/后沿被约束为等于 ScrollView 上的相同边缘。这里有一个关键:这些约束是针对 ScrollView 的 contentSize,而不是 viewcontroller 视图中显示的帧大小。所以它不是告诉 ContentView 与显示的 ScrollView 框架具有相同的框架大小,而是告诉 Scrollview ContentView 是它的内容,所以如果 contentview 大于 ScrollView 框架那么你会滚动,就像设置 scrollView.contentSize 更大比 scrollView.frame 使内容可滚动。

这是另一个关键:现在您必须在 ContentView、Label1-3 和除 Scrollview 之外的任何其他内容之间有足够的约束,以便 ContentView 能够从这些约束中计算出它的宽度和高度。

因此,例如,如果您想要一组垂直滚动的标签,您可以设置一个约束以使 ContentView 宽度等于 ViewController 视图的宽度,这会照顾宽度。要注意高度,请将 Label1 顶部固定到 ContentView 顶部,Label2 顶部固定到 Label1 底部,Label3 顶部固定到 Label2 底部,最后(重要的是)将 Label3 底部固定到 ContentView 底部。现在它有足够的信息来计算 ContentView 的高度。

我希望这能给某人一个线索,因为我阅读了上述帖子,但仍然无法弄清楚如何正确设置 ContentView 的宽度和高度约束。我缺少的是把 Label3 的底部固定到 ContentView 的底部,否则 ContentView 怎么知道它有多高(因为 Label3 会在那时浮动,并且没有约束告诉 ContentView 它的底部 y 位置在哪里)。

【讨论】:

  • 这部分让我很清楚“所以它不是告诉 ContentView 与显示的 ScrollView 框架的帧大小相同,而是告诉 Scrollview ContentView 是它的内容”。
  • 谢谢道格。你的“如果你想要一个垂直滚动的 []....你设置一个约束来使 ContentView 宽度等于 ViewController 视图的宽度,这会照顾宽度”让我很头疼。我将 ContentView 的宽度和高度都设置为等于 ViewController 的视图。我删除了相等的高度,这解决了很多约束问题。谢谢!
  • 很好的答案,没有人提到“将 Label3 的底部固定到 ContentView 的底部”。救了我的命!
【解决方案3】:

这是我如何使用容器视图布置纯自动布局 UIScrollView 的示例。我已发表评论以使其更清楚:

container 是标准 UIView,body 是 UITextView

- (void)viewDidLoad
{
    [super viewDidLoad];

    //add scrollview
    [self.view addSubview:self.scrollView];

    //add container view
    [self.scrollView addSubview:self.container];

    //body as subview of container (body size is undetermined)
    [self.container addSubview:self.body];

    NSDictionary *views = @{@"scrollView" : self.scrollView, @"container" : self.container, @"body" : self.body};
    NSDictionary *metrics = @{@"margin" : @(100)};

    //constrain scrollview to superview, pin all edges
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:kNilOptions metrics:metrics views:views]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:kNilOptions metrics:metrics views:views]];

    //pin all edges of the container view to the scrollview (i've given it a horizonal margin as well for my purposes)
    [self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[container]|" options:kNilOptions metrics:metrics views:views]];
    [self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[container]-margin-|" options:kNilOptions metrics:metrics views:views]];

    //the container view must have a defined width OR height, here i am constraining it to the frame size of the scrollview, not its bounds
    //the calculation for constant is so that it's the width of the scrollview minus the margin * 2
    [self.scrollView addConstraint:[NSLayoutConstraint constraintWithItem:self.container attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.scrollView attribute:NSLayoutAttributeWidth multiplier:1.0f constant:-([metrics[@"margin"] floatValue] * 2)]];

    //now as the body grows vertically it will force the container to grow because it's trailing edge is pinned to the container's bottom edge
    //it won't grow the width because the container's width is constrained to the scrollview's frame width
    [self.container addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[body]|" options:kNilOptions metrics:metrics views:views]];
    [self.container addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[body]|" options:kNilOptions metrics:metrics views:views]];
}

在我的示例中,“body”是一个 UITextView,但也可以是其他任何东西。如果您碰巧使用了 UITextView,请注意,为了使其垂直增长,它必须具有在 viewDidLayoutSubviews 中设置的高度约束。因此,在 viewDidLoad 中添加以下约束并保留对它的引用:

self.bodyHeightConstraint = [NSLayoutConstraint constraintWithItem:self.body attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:nil multiplier:1.0f constant:100.0f];
[self.container addConstraint:self.bodyHeightConstraint];

然后在 viewDidLayoutSubviews 中计算高度并更新约束的常量:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    [self.bodyHeightConstraint setConstant:[self.body sizeThatFits:CGSizeMake(self.container.width, CGFLOAT_MAX)].height];

    [self.view layoutIfNeeded];
}

需要第二次布局传递来调整 UITextView 的大小。

【讨论】:

    【解决方案4】:

    使用此代码。 ScrollView setContentSize 应该在主线程中调用异步。

    斯威夫特:

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
    
        DispatchQueue.main.async {
            var contentRect = CGRect.zero
    
            for view in self.scrollView.subviews {
               contentRect = contentRect.union(view.frame)
            }
    
            self.scrollView.contentSize = contentRect.size
        }
    }
    

    目标 C:

     - (void)viewDidLayoutSubviews {
         [super viewDidLayoutSubviews];
    
         dispatch_async(dispatch_get_main_queue(), ^ {
                            CGRect contentRect = CGRectZero;
    
                            for(UIView *view in scrollView.subviews)
                               contentRect = CGRectUnion(contentRect,view.frame);
    
                               scrollView.contentSize = contentRect.size;
                           });
    }
    

    【讨论】:

      【解决方案5】:

      滚动视图每时每刻都应该知道它的内容大小。内容大小是从滚动视图的子视图中推断出来的。将控制器属性映射到 xib 文件中描述子视图高度的约束非常方便。然后在代码(动画块)中,您可以更改这些约束属性的常量。如果您需要更改整个约束,请保留对它的引用,以便稍后在父容器中更新它。

      【讨论】:

      • 附上一些例子或代码。用文字表达自己的理解真的很容易。
      【解决方案6】:

      我的滚动视图变体 !Dynamic!身高:

      1) 将scroll view 添加到您的UIView。固定所有(顶部、底部、前导、尾随)约束。

      2) 将UIView 添加到Scroll View。固定所有(顶部、底部、前导、尾迹)约束。这将是您的Content view。也可以重命名。

      3) 控制从Content view 拖动到Scroll view - Equal width

      4) 将内容添加到您的 UIView。设置所需的约束。和!在下方添加底部约束 NOT Greater or equal (>=)(就像大多数人所说的)BUT Equal!例如,将其设置为 20

      在我的情况下,我的内容中有 UIImageView。我已将其height 连接到代码。如果我把它改成1000,滚动是可见的。一切正常。

      对我来说就像一个魅力。任何问题 - 欢迎来到 cmets。

      【讨论】:

      • 如果底部的item也在动态增长呢?
      • @jshapy8 我不确定,但我认为它会在滚动视图中调整 UIView 的大小。并且滚动视图会调整它的高度。你可以自己试试。我不能在atm做到这一点
      • 是的,我发现它确实会调整大小。我认为手动重绘动态视图的框架和它下面的视图是棘手的部分,而不仅仅是约束
      • @jshapy8 是的,我也在调整它的大小