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

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()
        }
    }
}
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)
        ]
    }
}