iOS Autolayout与UIScrollview:为什么滚动视图的内容视图没有填充滚动视图?

43

在viewDidLoad中调用以下代码会得到一个完全红屏的结果。我原本期望它是完全绿屏的,为什么会是红色呢?我该怎么做才能让它变成完全绿色呢?

UIScrollView* scrollView = [UIScrollView new];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.backgroundColor = [UIColor redColor];
[self.view addSubview:scrollView];

UIView* contentView = [UIView new];
contentView.translatesAutoresizingMaskIntoConstraints = NO;
contentView.backgroundColor = [UIColor greenColor];
[scrollView addSubview:contentView];

NSDictionary* viewDict = NSDictionaryOfVariableBindings(scrollView,contentView);

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:0 views:viewDict]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics:0 views:viewDict]];

[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:0 views:viewDict]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[contentView]|" options:0 metrics:0 views:viewDict]];
3个回答

73
滚动视图的约束与其他视图略有不同。 contentView 和其 superview(即 scrollView)之间的约束是相对于 scrollViewcontentSize 而不是其 frame。这可能看起来令人困惑,但实际上非常有用,这意味着您永远不必调整 contentSize,而是contentSize会自动调整以适应您的内容。此行为在Technical Note TN2154中有描述。
如果您想将 contentView 的大小定义为屏幕或类似的内容,则需要在contentView和主视图之间添加约束。尽管这与将内容放入滚动视图相反,但可以做到。
为了说明这个概念,即contentView的大小将由其内容驱动,而不是scrollViewbounds,请向您的contentView添加一个标签:
UIScrollView* scrollView = [UIScrollView new];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
scrollView.backgroundColor = [UIColor redColor];
[self.view addSubview:scrollView];

UIView* contentView = [UIView new];
contentView.translatesAutoresizingMaskIntoConstraints = NO;
contentView.backgroundColor = [UIColor greenColor];
[scrollView addSubview:contentView];

UILabel *randomLabel = [[UILabel alloc] init];
randomLabel.text = @"this is a test";
randomLabel.translatesAutoresizingMaskIntoConstraints = NO;
randomLabel.backgroundColor = [UIColor clearColor];
[contentView addSubview:randomLabel];

NSDictionary* viewDict = NSDictionaryOfVariableBindings(scrollView, contentView, randomLabel);

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView]|" options:0 metrics:0 views:viewDict]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[scrollView]|" options:0 metrics:0 views:viewDict]];

[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:0 views:viewDict]];
[scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[contentView]|" options:0 metrics:0 views:viewDict]];

[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[randomLabel]-|" options:0 metrics:0 views:viewDict]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[randomLabel]-|" options:0 metrics:0 views:viewDict]];

现在您可以看到,contentView(因此,scrollViewcontentSize)已经根据标准边距调整以适应标签。由于我没有指定标签的宽度/高度,所以标签的大小将根据您放入该标签的文本进行调整。
如果您希望 contentView 也根据主视图的宽度进行调整,您可以按如下方式重新定义 viewDict ,然后添加这些额外的约束条件(除了上面的所有其他约束条件):
UIView *mainView = self.view;

NSDictionary* viewDict = NSDictionaryOfVariableBindings(scrollView, contentView, randomLabel, mainView);

[mainView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[contentView(==mainView)]" options:0 metrics:0 views:viewDict]];

在滚动视图中,多行标签存在一个已知问题(错误?),如果你想让它根据文本量自动调整大小,你需要进行一些巧妙的处理,例如:

dispatch_async(dispatch_get_main_queue(), ^{
    randomLabel.preferredMaxLayoutWidth = self.view.bounds.size.width;
});

1
我似乎无法使标签在文本过长时垂直扩展以适应一行。我已将行数设置为0,并将lineBreakMode设置为NSLineBreakByWordWrapping。我是否漏掉了什么? - rdelmar
1
谢谢提供的链接。我尝试了Matt的解决方法,我认为他的子类化标签的方法更好--看起来不那么笨拙。特别是如果您添加多个标签,您可以将numberOfLines和setTranslates...方法以及layoutSubviews移动到其中,使代码更清晰。我还注意到,除非将该代码放在viewWillLayoutSubviews中(如Matt建议的那样),否则使用dispatch_async方法在旋转时无法获得标签的正确高度,这意味着您还需要为标签创建属性。 - rdelmar
我需要通过编程创建视图吗?我想在使用自动布局的Storyboard中将所有内容放在一个ViewController中,我该如何实现? - Jacek Kwiecień
@Xylian 不,你不需要以编程方式创建它,尽管如果你在IB中进行上述操作(至少在Xcode 5.0.1中),你需要在根视图和滚动视图之间再添加一个视图(因为IB似乎不允许你在滚动视图的子视图和根视图之间设置前导、尾随、顶部、底部对齐约束)。在IB中处理复杂约束总是在目标和IB对约束创建的(看似任意的)限制之间进行舞蹈。 - Rob
1
@Rob 谢谢!还可以考虑链接到苹果的技术说明2154,详细解释了这些内容 - ravron
显示剩余2条评论

1
我试了Rob的答案,一开始看起来很好。但是如果你也启用了缩放,这个自动布局代码就会妨碍。它在缩放时保持调整contentView的大小。也就是说,如果我放大了(zoomScale>1),我将无法滚动屏幕外的部分。
经过几天与自动布局的斗争,可悲的是我找不到任何解决办法。最后,这甚至都没有关系(什么?好吧,抱歉)。最后,我只是在contentView上删除自动布局(使用固定大小的contentView),然后在layoutSubviews中调整self.scrollView.contentInset。(我使用Xcode5,iOS 7)
我知道这不是对这个问题的直接解决方案。但我只想指出一个简单的解决方法。对于在scrollView中居中一个固定大小的contentView,只需两行代码即可完美工作!希望能帮助到某些人 ;)
- (void)layoutSubviews {
    [super layoutSubviews];

    // move contentView to center of scrollView by changing contentInset,
    // because I CANNOT set this with AUTOLAYOUT!!!
    CGFloat topInset = (self.frame.size.height - self.contentView.frame.size.height)/2;
    self.scrollView.contentInset = UIEdgeInsetsMake(topInset, 0, 0, 0);
}

-1
在一般情况下,自动布局认为视图的顶部、左侧、底部和右侧边缘是可见的边缘。也就是说,如果将一个视图固定在其父视图的左边缘,实际上是将它固定在父视图边界的最小x值上。改变父视图的边界原点不会改变视图的位置。
UIScrollView类通过改变其边界的原点来滚动其内容。为了使其与自动布局配合使用,滚动视图内部的上、左、下和右边缘现在表示其内容视图的边缘。
滚动视图的子视图上的约束必须产生填充大小,然后被解释为滚动视图的内容大小。(这不应与用于自动布局的intrinsicContentSize方法混淆。)要使用自动布局调整滚动视图的框架,约束必须明确指定滚动视图的宽度和高度,或者将滚动视图的边缘与其子树以外的视图连接起来。
请注意,您可以通过创建与滚动视图的子树之外的视图(如滚动视图的父视图)之间的约束,使滚动视图的子视图显示为悬浮(不滚动)在其他滚动内容上。
以下是两个配置滚动视图的示例,首先是混合方法,然后是纯粹的方法。

欢迎来到 Stack Overflow。请查看 如何撰写好的答案 - Rick

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