🐡

UIScrollViewとUIStackViewで快適なスクロールを(Swift)

2023/10/06に公開

はじめに

未だに UIScrollView を使うときに制約どうつけるんだ?と迷うのでまとめました。

こういうのができるようになります。

vertical_horizontal

レイアウトガイド

iOS 11 から Contetnt Layout GuideFrame Layout Guide が登場し ScrollView のレイアウトはかなり楽になりました。

それぞれ下記を表しています。

  • Contetnt Layout Guide:ScrollView の中身
  • Frame Layout Guide:ScrollView 自体

縦スクロール

Storyboard を使って縦スクロールができる画面をつくっていきます。

  1. Storyboard で View に UIScrollView をのせる
  2. UIScrollView の上下左右を Safe Area(もしくは super view)に合わせて制約をつける
  3. UIScrollView に UIStackView(Vertical)をのせる
  4. UIStackView の上下左右を Contetnt Layout Guide に合わせる
  5. UIStackView の幅を Frame Layout Guide に合わせる
  6. UIStackView の Instrinsic Size を Placeholder にする

こんな感じです。

vertical

placeholder

6 は UIStackView の高さが確定しておらず警告が出るのでそれの回避策として Placeholder を設定しています。

あとは UIStackView を ViewController に紐づけてコードでコンテンツをのせて完成。

final class ViewController: UIViewController {

    @IBOutlet private weak var stackView: UIStackView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let v1 = UIView()
        v1.backgroundColor = .systemRed
        stackView.addArrangedSubview(v1)
        v1.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v1.heightAnchor.constraint(equalToConstant: 100)])

        let v2 = UIView()
        v2.backgroundColor = .systemYellow
        stackView.addArrangedSubview(v2)
        v2.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v2.heightAnchor.constraint(equalToConstant: 200)])

        let v3 = UIView()
        v3.backgroundColor = .systemOrange
        stackView.addArrangedSubview(v3)
        v3.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v3.heightAnchor.constraint(equalToConstant: 900)])

        let v4 = UIView()
        v4.backgroundColor = .systemBlue
        stackView.addArrangedSubview(v4)
        v4.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v4.heightAnchor.constraint(equalToConstant: 300)])

        let v5 = UIView()
        v5.backgroundColor = .systemGreen
        stackView.addArrangedSubview(v5)
        v5.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v5.heightAnchor.constraint(equalToConstant: 100)])
    }
}

vertical

横スクロール

次は横スクロールができる画面をつくっていきます。

  1. Storyboard で View に UIScrollView をのせる
  2. UIScrollView の上下左右を Safe Area(もしくは super view)に合わせて制約をつける
  3. UIScrollView に UIStackView(Horizontal)をのせる
  4. UIStackView の上下左右を Contetnt Layout Guide に合わせる
  5. UIStackView の高さを Frame Layout Guide に合わせる
  6. UIStackView の Instrinsic Size を Placeholder にする

こんな感じです。

horizontal

あとは UIStackView を ViewController に紐づけてコードでコンテンツをのせて完成。

final class ViewController: UIViewController {

    @IBOutlet private weak var stackView: UIStackView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let v1 = UIView()
        v1.backgroundColor = .systemRed
        stackView.addArrangedSubview(v1)
        v1.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v1.widthAnchor.constraint(equalToConstant: 100)])

        let v2 = UIView()
        v2.backgroundColor = .systemYellow
        stackView.addArrangedSubview(v2)
        v2.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v2.widthAnchor.constraint(equalToConstant: 200)])

        let v3 = UIView()
        v3.backgroundColor = .systemOrange
        stackView.addArrangedSubview(v3)
        v3.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v3.widthAnchor.constraint(equalToConstant: 900)])

        let v4 = UIView()
        v4.backgroundColor = .systemBlue
        stackView.addArrangedSubview(v4)
        v4.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v4.widthAnchor.constraint(equalToConstant: 300)])

        let v5 = UIView()
        v5.backgroundColor = .systemGreen
        stackView.addArrangedSubview(v5)
        v5.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v5.widthAnchor.constraint(equalToConstant: 100)])
    }
}

horizontal

UIScrollViewのサブクラスを作ってやってみる

UIScrollView のサブクラスを作って汎用的にしてみます。

下記のように縦スクロール用と横スクロール用の 2 つのクラスを作成します。

final class VerticalScrollView: UIScrollView {

    private var stackView: UIStackView = {
        let stack = UIStackView()
        stack.axis = .vertical
        return stack
    }()

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

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    private func commonInit() {
        addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
            stackView.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor),
            stackView.widthAnchor.constraint(equalTo: frameLayoutGuide.widthAnchor)
        ])
    }

    func addContent(_ view: UIView) {
        stackView.addArrangedSubview(view)
    }
}

final class HorizontalScrollView: UIScrollView {

    private var stackView: UIStackView = {
        let stack = UIStackView()
        stack.axis = .horizontal
        return stack
    }()

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

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    private func commonInit() {
        addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
            stackView.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor),
            stackView.heightAnchor.constraint(equalTo: frameLayoutGuide.heightAnchor)
        ])
    }

    func addContent(_ view: UIView) {
        stackView.addArrangedSubview(view)
    }
}

Storyboard でこんな感じで配置します。

vh

コードでコンテンツをのせると完成!

final class ViewController: UIViewController {

    @IBOutlet private weak var verticalScrollView: VerticalScrollView!
    @IBOutlet private weak var horizontalScrollView: HorizontalScrollView!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupVertical()
        setupHorizontal()
    }

    private func setupVertical() {
        let v1 = UIView()
        v1.backgroundColor = .systemRed
        verticalScrollView.addContent(v1)
        v1.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v1.heightAnchor.constraint(equalToConstant: 100)])

        let v2 = UIView()
        v2.backgroundColor = .systemYellow
        verticalScrollView.addContent(v2)
        v2.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v2.heightAnchor.constraint(equalToConstant: 200)])

        let v3 = UIView()
        v3.backgroundColor = .systemOrange
        verticalScrollView.addContent(v3)
        v3.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v3.heightAnchor.constraint(equalToConstant: 900)])

        let v4 = UIView()
        v4.backgroundColor = .systemBlue
        verticalScrollView.addContent(v4)
        v4.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v4.heightAnchor.constraint(equalToConstant: 300)])

        let v5 = UIView()
        v5.backgroundColor = .systemGreen
        verticalScrollView.addContent(v5)
        v5.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v5.heightAnchor.constraint(equalToConstant: 100)])
    }

    private func setupHorizontal() {
        let v1 = UIView()
        v1.backgroundColor = .systemPurple
        horizontalScrollView.addContent(v1)
        v1.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v1.widthAnchor.constraint(equalToConstant: 100)])

        let v2 = UIView()
        v2.backgroundColor = .systemMint
        horizontalScrollView.addContent(v2)
        v2.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v2.widthAnchor.constraint(equalToConstant: 200)])

        let v3 = UIView()
        v3.backgroundColor = .systemPink
        horizontalScrollView.addContent(v3)
        v3.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v3.widthAnchor.constraint(equalToConstant: 900)])

        let v4 = UIView()
        v4.backgroundColor = .systemCyan
        horizontalScrollView.addContent(v4)
        v4.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v4.widthAnchor.constraint(equalToConstant: 300)])

        let v5 = UIView()
        v5.backgroundColor = .systemBrown
        horizontalScrollView.addContent(v5)
        v5.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([v5.widthAnchor.constraint(equalToConstant: 100)])
    }
}

おわりに

これでさくさくスクロールできるようになりました。

VerticalScrollViewHorizontalScrollView はコードでしかコンテンツをのせられないので使いやすいかはわかりません(ちなみに私はこういう書き方をしたことはないです🤥)。

まだまだ様々な理由で UIKit をはがせないプロジェクトもあるかと思いますががんばりましょう💪

Discussion