I just wasted an hour with what looks like an iOS 14 SDK regression when running on iOS 12 so I thought i\'d post this in case it helps others.
Summary:
To help understand UIScrollView
, Frame Layout Guide
and Content Layout Guide
...
Frame Layout Guide
and Content Layout Guide
were introduced in iOS 11, in part to eliminate the ambiguity of frame vs content, as well as to allow adding "non-scrolling" elements to the scroll view.
For each of the following examples, I'm adding and constraining the scroll view like this:
let scrollView: UIScrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView 20-pts on each side
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
// so we can see the scroll view's frame
scrollView.backgroundColor = .red
Now, consider the "old" way of adding content:
// call a func to get a vertical stack view with 30 labels
let stackView = createStackView()
scrollView.addSubview(stackView)
NSLayoutConstraint.activate([
// old method
// constrain stack view to scroll view itself
// this defines the "scrollable" content
// stack view Top / Leading / Trailing / Bottom to scroll view
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0.0),
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: 0.0),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 0.0),
// we want vertical scrolling, so we want our content to be only as wide as
// the scroll view itself
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: 0.0),
])
Fairly straightforward, although a little ambiguous. Am I making the stack view only as tall as the scroll view, by constraining its bottom anchor? That's what I'd expect if it's a subview of any other UIView
!
So, consider the "new" way of doing it:
// call a func to get a vertical stack view with 30 labels
let stackView = createStackView()
scrollView.addSubview(stackView)
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain stack view to scroll view's Content Layout Guide
// this defines the "scrollable" content
// stack view Top / Leading / Trailing / Bottom to scroll view's Content Layout Guide
stackView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
stackView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
stackView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
stackView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// we want vertical scrolling, so we want our content to be only as wide as
// the scroll view's Frame Layout Guide
stackView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
])
Now, it's very clear that I am using the stack view to define the "scrollable content" by constraining it to the .contentLayoutGuide
and using the scroll view's actual frame (its .frameLayoutGuide
) to define the stack view's width.
In addition, suppose I want a UI element - such as an image view - to be inside the scroll view, but I don't want it to scroll? The "old way" would require adding the image view as a sibling view overlaid on top of the scroll view.
But now, by using .frameLayoutGuide
, I can add it as a "non-scrolling" element inside the scroll view:
guard let img = UIImage(named: "scrollSample") else {
fatalError("could not load image")
}
let imgView = UIImageView(image: img)
imgView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(imgView)
NSLayoutConstraint.activate([
// constrain image view to the Frame Layout Guide
// at top-right, with 20-pts Top / Trailing "padding"
imgView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 20.0),
imgView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: -20.0),
// 120 x 120 pts
imgView.widthAnchor.constraint(equalToConstant: 120.0),
imgView.heightAnchor.constraint(equalToConstant: 120.0),
])
Now, any elements I constrain to the scrollView's .contentLayoutGuide
will scroll, but the image view constrained to the scrollView's .frameLayoutGuide
will remain in place.