-
RxSwift로 UITableView 구현하기🧑💻/Swift 2021. 8. 7. 19:28반응형
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
참고
반응형