RxSwift로 UITableView 구현하기
RxSwift를 사용하면 Delegate와 Delegate 프로토콜을 채택하여 구현했을 때 보다 코드의 양이 크게 줄어듭니다.
이 외에도 Thread 사용이 간편하다는 점 등 여러 장점이 있는데요.
먼저 Podfile에 RxSwift, RxCocoa를 추가하고 시작해 봅시다.
UITableViewDataSource
TableView를 구성하는 Cell 데이터를 관리하기 위한 프로토콜입니다.
스토리보드에 설정한 UITableView의 dataSource 연결을 해제합니다.
코드로 구현했었다면 UITableViewDataSource 프로토콜 연결을 제거하고 tableView.dataSource = self 코드를 제거합니다.
1개의 Section으로 구성된 기본 TableView
UITableViewDataSource에서 tableView(_:cellForRowAt:)로 구현하던 것을 RxCocoa의 tableView.rx.item로 구현했습니다.
Post 목록이 변경될 때마다 Cell을 새롭게 구성하므로 tableView.reloadData()가 호출되는 효과가 있습니다.
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
var posts = BehaviorRelay<[Post]>(value: [])
var disposeBag = DisposeBag()
override func viewDidLoad() {
// Cell 등록
let nibName = UINib(nibName: PostListTableViewCell.nibName, bundle: nil)
tableView.register(nibName, forCellReuseIdentifier: PostListTableViewCell.cellID)
tableView.rowHeight = UITableView.automaticDimension
// Cell 그리기
posts
.observe(on: MainScheduler.instance)
.bind(to: tableView.rx.items(cellIdentifier: PostListTableViewCell.cellID, cellType: PostListTableViewCell.self)) { index, item, cell in
cell.bind(post: item)
}
.disposed(by: disposeBag)
}
// ...
}
Cell이 재사용되므로 UITableViewCell 구현부에 prepareForReuse()를 override 합니다.
모든 subscribe를 종료할 수 있도록 전역변수로 선언한 disposeBag을 재할당해줍니다.
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
여러 Section으로 구성된 TableView
Section이 여러 개로 구성되어 있는 TableView를 구현할 땐, RxDataSources를 사용합니다.
Section이 1개였을 때와 달리 조금 복잡하네요.
1. Podfile에 RxDataSources 추가
저장하고 pod install 후 워크스페이스를 새로 엽니다.
2. SectionModelType 추가
Section을 구성하는 구조체를 생성합니다. 저는 날짜별로 Todo 목록을 표시하려 다음과 같이 구성했습니다.
import Foundation
import RxDataSources
struct Task {
var date: String
var items: [Todo]
}
extension Task: SectionModelType {
typealias Item = Todo
init(original: Task, items: [Item]) {
self = original
self.items = items
}
}
3. DataSource 추가
Cell을 그리기 위한 DataSource를 생성합니다.
titleForHeaderInSection으로 tableView(_:titleForHeaderInSection:)를 대체할 수 있습니다.
주의할 점은 sections에 dataSource를 설정했음에도 TableView에 DataSource를 설정하면 오류가 발생합니다.
tableView.dataSource = dataSource
RxCocoa/DelegateProxyType.swift:345: Assertion failed: Proxy changed from the time it was first set.
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
private var sections: BehaviorRelay<[Task]> = BehaviorRelay(value: [])
private var dataSource: RxTableViewSectionedReloadDataSource<Task>!
var disposeBag = DisposeBag()
override func viewDidLoad() {
// Cell 등록
let nibName = UINib(nibName: TodoTableViewCell.nibName, bundle: nil)
tblTodo.register(nibName, forCellReuseIdentifier: TodoTableViewCell.identifier)
tblTodo.rowHeight = UITableView.automaticDimension
// Cell 그리기
dataSource = RxTableViewSectionedReloadDataSource<Task> { dataSource, tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: TodoTableViewCell.identifier, for: indexPath) as! TodoTableViewCell
// cell 설정
cell.bind(task: item)
return cell
}
// Section 타이틀
dataSource.titleForHeaderInSection = { ds, index in
return ds.sectionModels[index].date
}
// 모든 Cell에 DataSource 연결
sections.asDriver()
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
// ...
}
4. Section 설정
Section을 BehaviorRelay로 선언했으므로 accept로 데이터를 전달합니다.
특정 이벤트나 데이터 값에 따라 Section을 달리 보여주고 싶다면 sections.accept를 호출해주면 됩니다.
기존에 있던 TableView의 reloadData()를 사용하지 않아도 자동으로 갱신됩니다.
sections.accept([Task(date: "2021-08-07", items: []),
Task(date: "2021-08-08", items: [])])
UITableViewDelegate
TableView에 대한 사용자 이벤트를 관리하기 위한 프로토콜입니다.
기존에 UITableViewDelegate를 연결하여 처리하던 작업을 RxCocoa를 사용해 바꿔봅니다.
스토리보드에 설정한 UITableView의 delegate 연결을 해제합니다.
코드로 구현했었다면 UITableViewDelegate 프로토콜 연결을 제거하고 tableView.delegate = self 코드를 제거합니다.
Cell 선택
itemSelected는 선택한 Cell의 indexPath를 return 하고, modelSelected는 선택한 Cell의 Model을 return 합니다.
Zip Operator를 사용하면 Cell 선택 시 indexPath와 Model을 묶어 한번에 처리할 수 있습니다.
/* Cell 선택 */
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// ...
}
Observable.zip(tableView.rx.modelSelected(Item.self), tableView.rx.itemSelected)
.bind { (item, indexPath) in
// ...
}
.disposed(by: disposeBag)
Cell 삭제
/* Cell 삭제 */
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
// ...
}
tableView.rx.itemDeleted
.bind { indexPath in
// ...
}
.disposed(by: disposeBag)
Cell 이동
/* Cell 순서 변경 시 조정 */
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// ...
}
tableView.rx.itemMoved
.bind { sourceIndexPath, destinationIndexPath in
// ...
}
.disposed(by: disposeBag)
프로젝트 진행하면서 사용한 TableView API들로 포스팅 해 보았는데 추후에 더 추가하려 합니다.
자세한 코드 내용은 저의 Github에서 참고하세요.
https://github.com/bigtoy2645/todoList-iOS