Scrollable View Controller, 말 그대로 스크롤이 가능한 View Controller는 어느 앱에나 다 들어간다. 이러한 Scrollable이 가능하도록 하는 View는 총 3가지가 있다. UIScrollView, UICollectionView, UITalbeView이다. 차근차근 알아보도록 하자.



UIScrollView

UIScrollView는 layout에 주목을 해서 봐야한다. 가장 root인 view에 대해 ScrollView의 layout을 잡게 되고, 그 ScrollView안에서 여러개의 StackView나 content들의 layout을 잡게 된다. 보통은 꽉 찬 화면으로 구현을 하기 때문에, StackView의 width와 ScrollView의 width는 동일하게 설정해주는 것이 포인트 이다.

import UIKit

class ViewController: UIViewController {

    let scrollView = UIScrollView()
    let stackView = UIStackView()

    override func viewDidLoad() {
        super.viewDidLoad()
        style()
        layout()
    }
}

extension ViewController {
    func style() {
        scrollView.translatesAutoresizingMaskIntoConstraints = false

        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.spacing = 20
    }

    func layout() {
        view.addSubview(scrollView)
        scrollView.addSubview(stackView)

        stackView.addArrangedSubview(makeCustomView())

        for _ in 0..<20 {
            stackView.addArrangedSubview(makeLabel())
        }

        // ScrollView
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        ])

        // StackView within the ScrollView
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
        ])

        // The magic constraint needed to make all this work
        stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
    }

    private func makeLabel() -> UILabel {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Welcome"
        label.textAlignment = .center
        label.backgroundColor = .systemGray
        return label
    }

    private func makeCustomView() -> UIView {
        let customView = CustomView()
        customView.translatesAutoresizingMaskIntoConstraints = false
        return customView
    }
}


class CustomView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .systemOrange
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var intrinsicContentSize: CGSize {
        return CGSize(width: UIView.noIntrinsicMetric, height: 200)
    }
}



UICollectionView

주로 TableView를 통해 구현을 하여, 얘는 그다지 사용을 많이 하지는 않는다고 한다. 간단하게만 알아보도록 하자.



UITableView

거의 모든 앱에서 사용되는 TableView이다. 이전에 나도 앱을 런칭할 때, Scroll이 들어가는 화면은 모두 TableView나 ScrollView를 통해 구현을 했었다. 보통 Header와 Footer가 있고, 그 사이에 vertical list로 cell이 들어가게 된다. 만약 cell이 늘어난다면 자동적으로 scrollable하게 된다.

UITableView의 장점으로는 성능이 좋고(reuseIdentifier), header나 footer, section에 내장된 다양한 편의성이 있다. 또한 single column 리스트에 적합하다.

단점으로는 만약 single column이 아닌 복잡한 column인 경우, layout을 잡기가 힘들다.


초기 화면은 TableView를 선택하였다. 아래의 화면과 같이 header를 구성하고, dummy data를 넣어보도록 하자.



AccountSummaryViewController

import UIKit

class AccountSummaryViewController: UIViewController {

    let games = [
        "Pacman",
        "Space Invaders",
        "Space Patrol",
    ]

    var tableView = UITableView()

    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }
}

extension AccountSummaryViewController {
    private func setup() {
        setupTableView()
        setupTableHeaderView()
    }

    private func setupTableView() {
        tableView.delegate = self
        tableView.dataSource = self

        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)

        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        ])
    }

    private func setupTableHeaderView() {
        let header = AccountSummaryHeaderView(frame: .zero) // no initial size

        var size = header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) // height
        size.width = UIScreen.main.bounds.width
        header.frame.size = size // width

        tableView.tableHeaderView = header
    }
}

extension AccountSummaryViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell()
        cell.textLabel?.text = games[indexPath.row]
        return cell
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return games.count
    }
}

extension AccountSummaryViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    }
}

TableView는 Data + Protocol Delegate가 기본 공식이다. 지금 여기서 Data는 games의 배열의 원소들이고, Protocol Delegate는 tableView.delegatet = self와 tableView.dataSource = self가 된다. 하나는 데이터를 위한 것이고, 하나는 tap을 하거나 어떠한 action이 들어오면 처리해주기 위한 것이다.

그런 다음 레이아웃 들을 잡아주고, setupTableHeaderView인 Header를 구성해준다. 나는 Header를 nib 파일을 통해 구성을 하였고, 이를 불러와야한다. size를 주목해 볼만 한데, 처음 var size = header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)는 높이를 지정하게 된다. nib 파일을 생성할 때에도 높이를 지정해 주었지, width는 지정하지 않았다. 이유는 width는 자동으로 잡히길 원하기 때문이다. iphone의 기종마다 크기가 다르기에, height만 잡아주는게 일반적이다. 밑의 header.frame.size = size 부분에서 breakpoint를 통해 확인해보면 여기서 width가 자동으로 잡히게 된다.

마지막으로 tableView에서는 꼭 설정을 해줘야 하는 부분이다. cellForRowAt과 numberOfRowsInSection을 설정해주게 된다. cellForRowAt은 어떠한 cell을 넣을것이냐에 대한 부분이고, numberOfRowsInSection은 몇개의 row을 넣을 것이냐에 대한 부분이다.

layout을 제외 하면, UIKit과 Storyboard에서의 TableView를 이용하는 방식이 크게 변하지 않다는 것을 느끼게 되었다.

이미지 출처 : https://github.com/jrasmusson/ios-professional-course/blob/main/Bankey/5-Scrollable-ViewControllers/README.md

태그:

카테고리:

업데이트: