UIScrollView 如何自适应子视图的高度?

| Swift , iOS

 

内容概览

  • 前言
  • 最终效果
  • UILayoutGuide
  • 采用 UILayoutGuide 来进行布局
  • 采用 frameLayoutGuide 和 contentLayoutGuide
  • 总结

 

前言

 

如今,在开发 iOS 应用的过程中,我们经常会采用 AutoLayout 来配置页面布局。然而,UIScrollView 这个家伙好像不好驾驭。别担心,Ficow 来陪你一起驯服这只倔驴子!

只要吃透本文中的几个重要概念,我相信您以后可以游刃有余地使用 AutoLayout 来配置 UIScrollView 的布局。

如果您未曾听说或者使用过 UILayoutGuide 以及 UIScrollView 的 frameLayoutGuidecontentLayoutGuide 属性,理解并掌握这些内容对您也会大有裨益。

 

最终效果

 

首先,请看最终效果(请注意右图中红色区域最右边的滚动条):

未达到最大高度,不可滑动 达到最大高度,可滑动
rigid scrollable

布局的需求:

  • 内容未达到最大高度限制时,UIScrollView 和子视图的高度一起增大,UIScrollView 的内容不可以滑动;
  • 内容达到最大高度限制时,UIScrollView 的高度为最大高度且不再增大,子视图高度继续增大,UIScrollView 的内容可以滑动;

这是可滑动时的效果动图:

在继续阅读后文之前,请您根据自己的经验思考一下,您会怎么实现这个需求呢?

如果想到了,欢迎您留言和 Ficow 交流,谢谢~
如果想不到,那也没关系。希望 Ficow 的实战总结可以对您有所帮助。如果错误,也欢迎您指正,谢谢~

 

UILayoutGuide

 

如果您已经掌握了 UILayoutGuide,您可以跳过这个小节。

首先,看官方文档的描述:

UILayoutGuide 是什么?

一个可以和 Auto Layout 交互的矩形框。

为什么要用 UILayoutGuide?

UILayoutGuide 可以解决用空白View进行布局时造成的问题:

  • 创建和维护空白View有一定的内存开销;
  • 空白View被添加到视图层级中,会加重视图执行任务的负担;
  • 空白View可能会意外拦截一些发到其他View的消息,造成难以发现的问题;

如何使用 UILayoutGuide?

// 创建 UILayoutGuide
let layoutGuide = UILayoutGuide()
// 添加到 view 里
view.addLayoutGuide(layoutGuide)
// 让 UILayoutGuide 参与到自动布局的工作中
let top = layoutGuide.topAnchor.constraint(equalTo: labelOne.topAnchor)
let bottom = layoutGuide.bottomAnchor.constraint(equalTo: labelTwo.bottomAnchor)
NSLayoutConstraint.activate([top, bottom])

 

利用 UILayoutGuide 来进行布局

 

以下为示例代码,复制到一个 ViewController 里并执行即可看到效果。
另外,您可以通过调节 textCount 的值来查看不同的效果。

func testScrollView() {
        let container = UIView()
        container.backgroundColor = .lightGray
        view.addSubview(container)

        let containerHeight = container.heightAnchor.constraint(equalToConstant: 500)
        containerHeight.priority = .defaultHigh // 与 view 的约束冲突,需要降低优先级
        let containerWidth = container.widthAnchor.constraint(equalTo: view.widthAnchor)
        containerWidth.priority = .defaultHigh // 与 view 的约束冲突,需要降低优先级
        NSLayoutConstraint.activate([
            container.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            container.topAnchor.constraint(lessThanOrEqualTo: view.topAnchor, constant: 100),
            containerWidth, containerHeight
        ])
        let scrollView = UIScrollView()
        scrollView.backgroundColor = .darkGray
        container.addSubview(scrollView)

        NSLayoutConstraint.activate([
            scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            scrollView.topAnchor.constraint(equalTo: container.topAnchor),
            scrollView.heightAnchor.constraint(lessThanOrEqualToConstant: 300) // 最大高度约束,优先级为 .required
        ])

        let textCount = Int(1e2) // 短文本
        // let textCount = Int(1e3) //长文本

        let labelOne = UILabel()
        labelOne.textColor = .black
        labelOne.numberOfLines = 0
        labelOne.backgroundColor = .red
        // 防止label的内容被挤压
        labelOne.setContentCompressionResistancePriority(.required, for: .vertical)
        labelOne.text = String(repeating: 1.description, count: textCount)

        let labelTwo = UILabel()
        labelTwo.textColor = .black
        labelTwo.numberOfLines = 0
        labelTwo.backgroundColor = .green
        // 防止label的内容被挤压
        labelTwo.setContentCompressionResistancePriority(.required, for: .vertical)
        labelTwo.text = String(repeating: 2.description, count: textCount)

        [labelOne, labelTwo].forEach(scrollView.addSubview(_:))
        NSLayoutConstraint.activate([
            labelOne.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            labelOne.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            labelOne.topAnchor.constraint(equalTo: scrollView.topAnchor),
            labelOne.widthAnchor.constraint(equalTo: view.widthAnchor)
        ])
        NSLayoutConstraint.activate([
            labelTwo.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            labelTwo.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            labelTwo.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor),
            labelTwo.widthAnchor.constraint(equalTo: labelOne.widthAnchor),
        ])
        let layoutGuide = UILayoutGuide()
        scrollView.addLayoutGuide(layoutGuide)
        // layoutGuide的顶部与labelOne的顶部要一致
        let top = layoutGuide.topAnchor.constraint(equalTo: labelOne.topAnchor)
        // layoutGuide的底部与labelTwo的底部要一致
        let bottom = layoutGuide.bottomAnchor.constraint(equalTo: labelTwo.bottomAnchor)
        NSLayoutConstraint.activate([top, bottom])

        // layoutGuide的高度要与scrollView的高度相等
        let height = scrollView.heightAnchor.constraint(equalTo: layoutGuide.heightAnchor)
        height.priority = .defaultHigh // scrollView 先满足最大高度约束,再满足与layoutGuide等高的约束
        NSLayoutConstraint.activate([height])
        [container, scrollView, labelOne, labelTwo].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
    }

注意事项:

  • 优先级的处理方式
    scrollView 最大高度为300的约束的优先级为 .required,而scrollView与layoutGuide等高约束的优先级为 .high。如此,就可以达到这样的目的:scrollView 先满足最大高度约束,再满足与layoutGuide等高的约束。

  • 防止label的内容被挤压
    label的内容有时候会因为约束被挤压,调用 setContentCompressionResistancePriority 方法配置label竖直方向上的抗挤压优先级。

最终,scrollView 就会根据内部子视图的高度自动调节自身的高度并同时满足最大高度的约束。

 

采用 frameLayoutGuide 和 contentLayoutGuide

 

其实,我们不需要为 UIScrollView 再添加 UILayoutGuide。UIScrollView 本身就有 frameLayoutGuidecontentLayoutGuide 属性。

这是官方文档对这两个属性的描述:

请注意 untransformed 这个形容词,如果 UIScrollView 的 transform 已被更改,就会导致最终结果与您的预期不符。
另外,添加约束到 UIScrollView 本身添加约束到 UIScrollView 的 frameLayoutGuide 也是不同的,如果您遭遇了bug,可以尝试将二者互换。

现在,让我们用 frameLayoutGuidecontentLayoutGuide 来改写之前的代码:

func testScrollView2() {
        let container = UIView()
        container.backgroundColor = .lightGray
        view.addSubview(container)

        let containerHeight = container.heightAnchor.constraint(equalToConstant: 500)
        containerHeight.priority = .defaultHigh // 与 view 的约束冲突,需要降低优先级
        let containerWidth = container.widthAnchor.constraint(equalTo: view.widthAnchor)
        containerWidth.priority = .defaultHigh // 与 view 的约束冲突,需要降低优先级
        NSLayoutConstraint.activate([
            container.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            container.topAnchor.constraint(lessThanOrEqualTo: view.topAnchor, constant: 100),
            containerWidth, containerHeight
        ])

        let scrollView = UIScrollView()
        scrollView.backgroundColor = .darkGray
        container.addSubview(scrollView)

        NSLayoutConstraint.activate([
            scrollView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            scrollView.topAnchor.constraint(equalTo: container.topAnchor),
            scrollView.heightAnchor.constraint(lessThanOrEqualToConstant: 300) // 最大高度约束,优先级为 .required
        ])

        let textCount = Int(1e3)

        let labelOne = UILabel()
        labelOne.textColor = .black
        labelOne.numberOfLines = 0
        labelOne.backgroundColor = .red
        // 防止label的内容被挤压
        labelOne.setContentCompressionResistancePriority(.required, for: .vertical)
        labelOne.text = String(repeating: 1.description, count: textCount)

        let labelTwo = UILabel()
        labelTwo.textColor = .black
        labelTwo.numberOfLines = 0
        labelTwo.backgroundColor = .green
        // 防止label的内容被挤压
        labelTwo.setContentCompressionResistancePriority(.required, for: .vertical)
        labelTwo.text = String(repeating: 2.description, count: textCount)

        [labelOne, labelTwo].forEach(scrollView.addSubview(_:))
        NSLayoutConstraint.activate([
            labelOne.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            labelOne.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            labelOne.topAnchor.constraint(equalTo: scrollView.topAnchor),
            labelOne.widthAnchor.constraint(equalTo: view.widthAnchor)
        ])
        NSLayoutConstraint.activate([
            labelTwo.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            labelTwo.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            labelTwo.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor),
            labelTwo.widthAnchor.constraint(equalTo: labelOne.widthAnchor),
        ])

        // 改写部分
        let height = scrollView.contentLayoutGuide.heightAnchor.constraint(equalTo: scrollView.frameLayoutGuide.heightAnchor)
        height.priority = .defaultHigh
        height.isActive = true

        [container, scrollView, labelOne, labelTwo].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
    }

改写之后,UILayoutGuide 相关的代码被进一步简化。

 

总结

 

简而言之,就两个要点:

  1. 使用 UILayoutGuide 进行布局,优于使用空白View进行布局;
  2. 使用约束对 UIScrollView 进行布局时,可以考虑使用 frameLayoutGuide 和 contentLayoutGuide;

 

参考内容:
UILayoutGuide
frameLayoutGuide
contentLayoutGuide

 

觉得不错?点个赞呗~

本文链接:UIScrollView 如何自适应子视图的高度?

转载声明:本站文章如无特别说明,皆为原创。转载请注明:Ficow Shen's Blog

评论区(期待你的留言)