호댕의 iOS 개발

[TableView] 코드로 TableView 구현하기, convenience init이 호출이 안된다?! (+ register와 dequeueReusableCell 쉽게 하기) 본문

Software Engineering/iOS

[TableView] 코드로 TableView 구현하기, convenience init이 호출이 안된다?! (+ register와 dequeueReusableCell 쉽게 하기)

호르댕댕댕 2022. 5. 31. 23:07

최근 Rx를 통해 items를 Bind하거나, DiffableDataSource, Compositional Layout을 통해 리스트를 구현하다보니 TableView를 정석(UITableViewDataSource를 사용하는 방법)으로 구현하는 방법이 가물가물했다. 

 

진짜 한동안 안쓰다 보면 머리 속에서 희석되는... 그래서 다시 사용한 김에 정리를 해보고자 한다.

 

기존 rx를 사용한 방식은 쉽게 tableView에 item을 넣어줄 수 있다. 

viewModel.list.observe(on: MainScheduler.instance)
    .bind(to: tableView.rx.items(
        cellIdentifier: String(describing: ProjectTableViewCell.self),
        cellType: ProjectTableViewCell.self
    )) { [weak self] _, item, cell in
        cell.configureCellContent(for: item)
        cell.setupData(work: item)

        cell.delegate = self
    }
    .disposed(by: disposeBag)

하지만 이렇게 구현을 해주는 경우 Section이 여러 종류가 있다면 이에 대응을 할 수 없다. 

RxDataSource를 사용하면 이에 대응을 할 수 있지만 이번에는 UITableViewDataSource를 통해 구현을 하는 방법을 선택했다. 

 

TableView를 위한 메서드를 좀 더 쉽게 사용하기 

기존 TableView를 사용하다보면 register, dequeueReusableCell 같은 메서드를 사용해야 한다. 하지만 여기에 매번 Cell의 identifier를 넣어주는 것은 아주 번거롭다. 따라서 아래와 같이 extension에 구현을 해놓으면 보다 쉽게 셀 등록과 dequeue를 할 수 있다. 

import UIKit

extension UITableView {
    func register<T: UITableViewCell>(cellType: T.Type) {
        register(cellType, forCellReuseIdentifier: String(describing: cellType))
    }
    
    func dequeueReusableCell<T: UITableViewCell>(of cellType: T.Type, for indexPath: IndexPath) -> T {
        guard let cell = dequeueReusableCell(withIdentifier: String(describing: cellType), for: indexPath) as? T else {
            return T()
        }
        return cell
    }
}

cellType에는 Generic으로 UITableViewCell에 해당하는 타입만 올 수 있도록 하고 reuseIdentifier는 String(describing:)을 통해 타입 이름과 동일하게 정해주는 것이다. 

 

이렇게 하면 CellType만 지정해주어, Cell을 좀 더 쉽게 사용할 수 있다. 

 

Cell의 Init이 호출이 안된다?!

이렇게 한 후 UITableViewDataSource에서 Section 수와 각 섹션에 들어가는 Row의 수를 정해주고 tableView(:cellForRowAt:) 메서드를 구현해주었다. 

 

해당 메서드의 경우 TableView의 특정 위치에 들어갈 셀에 대한 DataSource를 요청한다. 즉, 셀에 어떤 내용이 들어갈 지 정해주는 메서드이다. 

이 내부에서 dequeueReusableCell(withIdentifier:for:)를 사용해 셀을 생성하거나 재사용하게 된다. 셀을 생성한 후 적합한 값으로 셀을 업데이트하는 것이다. 

 

그런데 커스텀 Cell의 convenience init이 호출되지 않았다. 그래서 셀의 UI가 제대로 배치가 되지 않는 문제가 있었다. 

convenience init() {
    self.init()
    configureUI()
}

이런 식으로 했지만 self.init()이 호출되지 않았고 convenience init() 또한 호출되지 않았다. 

그래서 UITableViewCell의 공식문서를 다시 살펴보았다. 

 

여기서 보니 Creating a Table View Cell에 init(style:reuseIdentifier:) 이니셜라이저가 있었다. 이는 UITableViewCell의 지정 이니셜라이저였다.

문서를 조금 더 자세히 살펴보니 style과 reuseIdentifier를 사용하여 테이블 셀을 초기화하고 호출자에게 이를 반환한다고 되어 있었다. 

 

그렇다면 호출자는 누구지??

tableView(:cellForRowAt:) 메서드에서 dequeueReusableCell(withIdentifier:) 메서드를 호출하여 현재 row에서 사용할 Cell 객체를 얻는다고 하고 있었다. 

dequeueReusableCell(withIdentifier:) 메서드를 호출하면 해당 init이 호출되어 Cell 객체를 생성한 뒤 이를 반환하는 것이다. 

 

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    configureUI()
}

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

그래서 해당 Initializer를 사용했더니 Cell을 dequeue하는 시점에서 잘 초기화가 되었다! 

 

 

이외에도 translatesAutoresizingMaskIntoConstraints를 false로 바꾸지 않아 레이아웃이 이상하게 잡히는 문제가 있었다... ㅎ...

항상 있지 않고 AutoLayout을 코드로 잡아주는 경우 해당 프로퍼티(translatesAutoresizingMaskIntoConstraints)를 false로 바꿔주자... 

Comments