具有动态大小内容的 UIScrollView

Posted

技术标签:

【中文标题】具有动态大小内容的 UIScrollView【英文标题】:UIScrollView with dynamically sized content 【发布时间】:2020-05-05 14:59:47 【问题描述】:

(Xcode 11,斯威夫特)

作为 ios 和 Autolayout 的新手,我正在努力实现一个相当简单的(恕我直言)视图,该视图显示 [垂直] 项目列表。唯一的问题是项目是动态决定的,每个项目都可以是文本或图像(其中任何一个都可能相当大,因此需要滚动)。 WebView 不是一个选项,所以它必须在本地实现。

我是这样理解这个过程的:

在 IB 中创建一个 UIScrollView 并将其调整为外框的大小。 将容器视图创建为 UIScrollView 的子视图(同样,在 IB 中),并将其大小设置为相同。 设置两个宽度相等的约束 在运行时,使用 UILabels/UIImageViews 填充容器视图,并以编程方式设置约束以确保正确的布局。 “告诉”滚动视图关于子视图的高度,以使其管理其滚动。

这是正确的方法吗?它似乎对我不起作用(对于一个将非常高的图像动态添加到容器视图的玩具示例 - 我无法让滚动工作)。在上述过程中执行最后一步的正确方法是什么 - 只需将滚动视图的 contentSize 强制为填充容器视图的大小(它似乎对我不起作用)。任何帮助将不胜感激。

【问题讨论】:

【参考方案1】:

在运行时向滚动视图添加多个元素时,您可能会发现使用UIStackView 会更容易...如果设置正确,它会随着每个添加的对象自动增加高度。

举个简单的例子……

1) 首先添加一个UIScrollView(我给它一个蓝色背景以便于查看)。将其在所有 4 个方面都约束为零:

请注意,我们看到“红色圆圈”表示缺少/冲突的约束。暂时忽略它。

2) 在滚动视图中添加一个UIView 作为“内容视图”(我给它一个systemYellow 背景以便于查看)。在内容布局指南的所有 4 个方面将其限制为零——这将(最终)定义滚动视图的内容大小。还要将其限制为等宽和等高框架布局指南

重要步骤:选择高度约束,然后在Size Inspector 窗格中选择Placeholder - Remove at build time 复选框。这将在设计时满足 IB 中的自动布局,但将允许该视图的高度根据需要缩小/增长。

3) 将垂直UIStackView 添加到“内容视图”。将其在所有 4 个面上都约束为零。将其属性配置为Fill / Fill / 8(如下图):

4) 在视图控制器类的堆栈视图中添加一个@IBOutlet 连接。现在,在运行时,当您将 UI 元素添加到堆栈视图时,您的所有“可滚动性”都将由自动布局处理。

这是一个示例类:

class DynaScrollViewController: UIViewController 

    @IBOutlet var theStackView: UIStackView!

    override func viewDidLoad() 
        super.viewDidLoad()

        // local var so we can reuse it
        var theLabel = UILabel()
        var theImageView = UIImageView()

        // create a new label
        theLabel = UILabel()
        // this gets set to false when the label is added to a stack view,
        // but good to get in the habit of setting it
        theLabel.translatesAutoresizingMaskIntoConstraints = false
        // multi-line
        theLabel.numberOfLines = 0
        // cyan background to make it easy to see
        theLabel.backgroundColor = .cyan
        // add 9 lines of text to the label
        theLabel.text = (1...9).map( "Line \($0)" ).joined(separator: "\n")

        // add it to the stack view
        theStackView.addArrangedSubview(theLabel)

        // add another label
        theLabel = UILabel()
        // multi-line
        theLabel.numberOfLines = 0
        // yellow background to make it easy to see
        theLabel.backgroundColor = .yellow
        // add 5 lines of text to the label
        theLabel.text = (1...5).map( "Line \($0)" ).joined(separator: "\n")

        // add it to the stack view
        theStackView.addArrangedSubview(theLabel)

        // create a new UIImageView
        theImageView = UIImageView()
        // this gets set to false when the label is added to a stack view,
        // but good to get in the habit of setting it
        theImageView.translatesAutoresizingMaskIntoConstraints = false
        // load an image for it - I have one named background
        if let img = UIImage(named: "background") 
            theImageView.image = img
        
        // let's give the image view a 4:3 width:height ratio
        theImageView.widthAnchor.constraint(equalTo: theImageView.heightAnchor, multiplier: 4.0/3.0).isActive = true

        // add it to the stack view
        theStackView.addArrangedSubview(theImageView)

        // add another label
        theLabel = UILabel()
        // multi-line
        theLabel.numberOfLines = 0
        // yellow background to make it easy to see
        theLabel.backgroundColor = .green
        // add 2 lines of text to the label
        theLabel.text = (1...2).map( "Line \($0)" ).joined(separator: "\n")

        // add it to the stack view
        theStackView.addArrangedSubview(theLabel)

        // add another UIImageView
        theImageView = UIImageView()
        // this gets set to false when the label is added to a stack view,
        // but good to get in the habit of setting it
        theImageView.translatesAutoresizingMaskIntoConstraints = false
        // load a different image for it - I have one named AquariumBG
        if let img = UIImage(named: "AquariumBG") 
            theImageView.image = img
        
        // let's give this image view a 1:1 width:height ratio
        theImageView.heightAnchor.constraint(equalTo: theImageView.widthAnchor, multiplier: 1.0).isActive = true

        // add it to the stack view
        theStackView.addArrangedSubview(theImageView)

    


如果已遵循这些步骤,您应该会得到以下输出:

并且,在滚动到底部之后:

【讨论】:

【参考方案2】:

对齐约束(前导/尾随/顶部/底部)

滚动视图和内容视图之间的对齐约束定义了scrollable range of the content例如

如果scrollView.bottom = contentView.bottom,则表示Scroll View是 可滚动到内容视图的底部。 如果 scrollView.bottom = contentView.bottom + 100,则可滚动 Scroll View 的底端将超过 Content View 的端 100 分。 如果scrollView.bottom = contentView.bottom — 100,则底部 即使滚动视图滚动到内容视图也不会到达 底端。

即Scroll View上的(底部)anchor表示外框的(底部)边缘,即Content View的可见部分; Content View 上的(底部)锚点是指实际内容的边缘,如果不滚动到该边缘,它将被隐藏。 与普通用例不同,Scroll View 和 Content View 之间的对齐约束与 Content View 的实际大小无关。它们只影响“内容视图的可滚动范围”,而不影响“实际内容大小”。 Content View 的实际大小必须另外定义。

尺寸限制(宽度/高度)

要实际调整 Content View 的大小,我们可以将 Content View 的大小设置为特定的长度,比如宽/高为 500。如果宽/高超过了 Scroll View 的宽/高,就会有一个滚动条供用户使用滚动。 但是,更常见的情况是,我们希望 Content View 与 Scroll View 具有相同的宽度(或高度)。在这种情况下,我们将有

contentView.width = scrollView.width

Content View 的宽度是指内容的实际全宽。另一方面,Scroll View 的宽度是指 Scroll View 的外部容器框架宽度。当然,不一定要等宽,也可以是a * scrollView.width + b等其他形式。 如果我们的 Content View 比 Scroll View 更高(或更宽),就会出现一个滚动条。 内容视图不仅可以是单个视图,还可以是多个视图,只要它们使用 Scroll View 的对齐和大小约束进行适当的约束即可。

详情可以关注这篇文章:Link。

【讨论】:

以上是关于具有动态大小内容的 UIScrollView的主要内容,如果未能解决你的问题,请参考以下文章

如何根据其中的内容创建具有动态高度的集合视图?

具有动态大小单元格间距问题的 UICollectionViewCell

具有动态大小的单元格的复杂自动布局

使用动态增加单元格计算表格视图的内容大小

具有自动布局的 UIScrollView 中的动态大小的 UITableView

在具有自动布局的 UITableViewCell 中动态调整大小的 UIWebView - 违反约束