관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] TableView - section, header, footer 사용방법 본문

iOS 응용 (swift)

[iOS - swift] TableView - section, header, footer 사용방법

jake-kim 2021. 7. 27. 23:21

Header, Section, Footer 개념

  • Section이 존재하고 Section 하나당 각각 Header와 Footer가 존재
  • 예제에서는 Header가 Section1에서만 표출
  • Footer는 Section3에서만 표출하지 않는 형태

데이터 Entity 정의

import Foundation

typealias MyItemList = [MyItem]

struct MyItem {
    enum MyType {
        case a
        case b

        case c
        case d
        case e
    }

    let title: String
    let type: MyType
}

BaseView 정의

  • BaseTableViewCell
class BaseTableViewCell<T>: UITableViewCell {
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        configure()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure() {
        selectionStyle = .none
    }

    var model: T? {
        didSet {
            if let model = model {
                bind(model)
            }
        }
    }

    func bind(_ model: T?) {}
}
  • BaseTableViewHeaderFooterView
class BaseTableViewHeaderFooterView<T>: UITableViewHeaderFooterView {
    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)

        configure()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    var model: T? {
        didSet {
            if let model = model {
                bind(model)
            }
        }
    }

    func configure() {}
    func bind(_ model: T) {}
}

CustomView 정의

  • MyTableViewCell
import UIKit
import SnapKit

class MyTableViewCell: BaseTableViewCell<MyItem> {

    lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 16)
        label.textColor = .black

        return label
    }()

    override func configure() {
        super.configure()

        backgroundColor = .white

        addSubviews()
        makeConstraints()
    }

    private func addSubviews() {
        contentView.addSubview(titleLabel)
    }

    private func makeConstraints() {
        titleLabel.snp.makeConstraints { maker in
            maker.centerY.equalToSuperview()
            maker.leading.equalToSuperview().inset(24)
        }
    }

    override func bind(_ model: MyItem?) {
        super.bind(model)

        titleLabel.text = model?.title
    }
    
}
  • MyTableViewHeader
import UIKit
import RxSwift

class MyTableViewHeader: BaseTableViewHeaderFooterView<Void> {

    var disposeBag = DisposeBag()

    private lazy var containerView: UIView = {
        let view = UIView()
        view.backgroundColor = .white

        return view
    }()

    private lazy var myImageView: UIImageView = {
        let view = UIImageView()
        view.image = #imageLiteral(resourceName: "background")

        return view
    }()

    private lazy var disclosureIndicatorButton: UIButton = {
        let button = UIButton()
        button.setImage(#imageLiteral(resourceName: "btnDisclosureIndicator"), for: .normal)

        return button
    }()

    private lazy var separatorView: UIView = {
        let view = UIView()
        view.backgroundColor = .separator

        return view
    }()

    override func configure() {
        super.configure()

        backgroundColor = .white
        addSubviews()
        makeConstraints()
    }

    private func addSubviews() {
        addSubview(containerView)
        containerView.addSubview(myImageView)
        containerView.addSubview(disclosureIndicatorButton)
        containerView.addSubview(separatorView)
    }

    private func makeConstraints() {
        containerView.snp.makeConstraints { maker in
            maker.edges.equalToSuperview()
        }

        myImageView.snp.makeConstraints { maker in
            maker.top.greaterThanOrEqualToSuperview().inset(24)
            maker.leading.equalToSuperview().inset(24)
            maker.bottom.equalTo(separatorView).offset(-24)
        }

        disclosureIndicatorButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        disclosureIndicatorButton.snp.makeConstraints { maker in
            maker.trailing.equalToSuperview().inset(24)
            maker.centerY.equalTo(myImageView)
            maker.leading.greaterThanOrEqualTo(myImageView.snp.trailing).offset(30)
        }

        separatorView.snp.makeConstraints { maker in
            maker.height.equalTo(1)
            maker.leading.trailing.equalToSuperview().inset(16)
            maker.bottom.equalToSuperview().inset(12)
        }
    }
}
  • SeparatorTableViewFooter
class SeparatorTableViewFooter: BaseTableViewHeaderFooterView<Void> {

    lazy var separatorView: UIView = {
        let view = UIView()
        view.backgroundColor = .separator

        return view
    }()

    override func configure() {
        super.configure()

        addSubviews()
        makeConstraints()
    }

    private func addSubviews() {
        contentView.addSubview(separatorView)
    }

    private func makeConstraints() {
        separatorView.snp.makeConstraints { maker in
            maker.height.equalTo(1)
            maker.leading.trailing.greaterThanOrEqualToSuperview().inset(16)
            maker.centerY.equalToSuperview()
        }
    }
}

tableView 적용

  • 핵심 
    • tableView.register()를 Header, Cell, Footer 각각 해주는 부분
    • Header와 Footer 각각 생성하고 height 설정하는 delegate 메서드
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {

    // business logic에 위치해야할 값
    var dataSource: [MyItemList] = []

    lazy var tableView: UITableView = {

        // header 영역도 같이 scroll되기 위해 .grouped로 설정
        let view = UITableView(frame: .zero, style: .grouped)
        view.dataSource = self
        view.delegate = self
        view.estimatedRowHeight = 44
        view.rowHeight = 48
        view.separatorStyle = .none
        view.backgroundColor = .white
        view.register(MyTableViewCell.self, forCellReuseIdentifier: "MyTableViewCell")
        view.register(MyTableViewHeader.self, forHeaderFooterViewReuseIdentifier: "MyTableViewHeader")
        view.register(SeparatorTableViewFooter.self, forHeaderFooterViewReuseIdentifier: "SeparatorTableViewFooter")

        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        setupDataSource()
        addSubviews()
        configure()
    }

    private func addSubviews() {
        view.addSubview(tableView)
    }

    private func configure() {
        tableView.snp.makeConstraints { maker in
            maker.leading.top.bottom.equalToSuperview()
            maker.trailing.equalToSuperview().inset(120)
        }
    }
}

// business logic에 위치해야할 값들
extension ViewController {

    var numberOfSections: Int {
        return dataSource.count
    }

    func setupDataSource() {
        // Section 1
        let item1 = MyItem(title: "1", type: .a)
        let item2 = MyItem(title: "2", type: .b)

        // Section 2
        let item3 = MyItem(title: "3", type: .c)
        let item4 = MyItem(title: "4", type: .d)
        let item5 = MyItem(title: "5", type: .e)

        // Section 3
        let item6 = MyItem(title: "6", type: .c)
        let item7 = MyItem(title: "7", type: .d)
        let item8 = MyItem(title: "8", type: .e)

        let section1 = [item1, item2]
        let section2 = [item3, item4, item5]
        let section3 = [item6, item7, item8]

        dataSource = [section1, section2, section3]
    }

    func numberOfRows(in section: Int) -> Int {
        return dataSource[section].count
    }

    func getItem(in section: Int, _ row: Int) -> MyItem {
        return dataSource[section][row]
    }

    func didSelectRowAt(in section: Int, _ row: Int) {
        print("did select type = \(dataSource[section][row].type)")
    }
}
  • 핵심: dataSource, delegate
    • Header와 Footer도 일반 Cell과 동일하게 객체를 만들고 반환 및 height 조정
extension ViewController: UITableViewDataSource, UITableViewDelegate {

    // TableView DataSource

    func numberOfSections(in tableView: UITableView) -> Int {
        return numberOfSections
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return numberOfRows(in: section)
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell") as? MyTableViewCell else {
            fatalError("MyTableViewCell not dequeued property")
        }
        let item = getItem(in: indexPath.section, indexPath.row)
        cell.model = item

        return cell
    }

    // Header & Footer

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        guard section == 0, let headerCell = tableView.dequeueReusableHeaderFooterView(withIdentifier: "MyTableViewHeader") as? MyTableViewHeader else { return nil }

        let tapGesture = UITapGestureRecognizer()
        headerCell.addGestureRecognizer(tapGesture)
        tapGesture.rx.event
            .asDriver()
            .drive(onNext: { _ in
                print("did tap first header")
            }).disposed(by: headerCell.disposeBag)

        return headerCell
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        // Header 영역 크기 = 140(separator 상단) + 12(separator 하단)

        return section == 0 ? 152 : 0
    }

    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        // 마지막 section은 footer 미표출

        guard section != numberOfSections - 1 else {
            return nil
        }
        return tableView.dequeueReusableHeaderFooterView(withIdentifier: "SeparatorTableViewFooter")
    }

    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        // footer 영역 크기 = 12 (마지막 section의 footer 크기는 0)

        return section == numberOfSections - 1 ? 0 : 12
    }

    // Select Cell

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        didSelectRowAt(in: indexPath.section, indexPath.row)
    }
}

* source code: https://github.com/JK0369/TableViewEx

Comments