How to make scrolling UIScrollView with Auto Layout in iOS

Issue #371

Scrolling UIScrollView is used in common scenarios like steps, onboarding.
From iOS 11, UIScrollView has contentLayoutGuide and frameLayoutGuide

Docs

https://developer.apple.com/documentation/uikit/uiscrollview/2865870-contentlayoutguide

Use this layout guide when you want to create Auto Layout constraints related to the content area of a scroll view.

https://developer.apple.com/documentation/uikit/uiscrollview/2865772-framelayoutguide

Use this layout guide when you want to create Auto Layout constraints that explicitly involve the frame rectangle of the scroll view itself, as opposed to its content rectangle.

Code

I found out that using contentLayoutGuide and frameLayoutGuide does not work in iOS 11, when swiping to the next page, it breaks the constraints. iOS 12 works well, so we have to check iOS version

Let the contentView drives the contentSize of scrollView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import UIKit

final class PagerView: UIView {
let scrollView = UIScrollView()

private(set) var pages: [UIView] = []
private let contentView = UIView()

override init(frame: CGRect) {
super.init(frame: frame)

setup()
}

required init?(coder aDecoder: NSCoder) {
fatalError()
}

private func setup() {
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false

addSubview(scrollView)
scrollView.addSubview(contentView)

if #available(iOS 12.0, *) {
scrollView.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.on([
scrollView.frameLayoutGuide.pinEdges(view: self)
])

NSLayoutConstraint.on([
scrollView.contentLayoutGuide.pinEdges(view: contentView),
[scrollView.contentLayoutGuide.heightAnchor.constraint(
equalTo: scrollView.frameLayoutGuide.heightAnchor
)]
])
} else {
NSLayoutConstraint.on([
scrollView.pinEdges(view: self),
scrollView.pinEdges(view: contentView)
])

NSLayoutConstraint.on([
contentView.heightAnchor.constraint(equalTo: heightAnchor)
])
}
}

func update(pages: [UIView]) {
clearExistingViews()

self.pages = pages
setupConstraints()
}

private func setupConstraints() {
pages.enumerated().forEach { tuple in
let index = tuple.offset
let page = tuple.element

contentView.addSubview(page)

NSLayoutConstraint.on([
page.topAnchor.constraint(equalTo: scrollView.topAnchor),
page.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
page.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
])

if index == 0 {
NSLayoutConstraint.on([
page.leftAnchor.constraint(equalTo: contentView.leftAnchor)
])
} else {
NSLayoutConstraint.on([
page.leftAnchor.constraint(equalTo: pages[index - 1].rightAnchor)
])
}

if index == pages.count - 1 {
NSLayoutConstraint.on([
page.rightAnchor.constraint(equalTo: contentView.rightAnchor)
])
}
}
}

private func clearExistingViews() {
pages.forEach {
$0.removeFromSuperview()
}
}
}
1
2
3
4
5
6
7
8
9
10
extension UILayoutGuide {
func pinEdges(view: UIView, inset: UIEdgeInsets = UIEdgeInsets.zero) -> [NSLayoutConstraint] {
return [
leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: inset.left),
trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: inset.right),
topAnchor.constraint(equalTo: view.topAnchor, constant: inset.top),
bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: inset.bottom)
]
}
}

Comments