안녕하세요 이누입니다.
오늘은 RxCocoa를 기반으로 TableView를 구현하는 방법에 대해 정리해보았습니다.
개인적으로 UITableViewDataSource같은 Protocol을 채택하고 구현하지 않아도 되어서 좋더라구요!
그럼 시작합니다.
RxCocoa - TableView 구현
기존에 TableView의 Cell을 구현하기 위해서는 UITableViewDelegate 객체 활용이 필수적이었습니다.
하지만 RxCocoa를 활용하면 DataSource 없이 메서드를 기반으로 Cell 구현이 가능합니다.
RxCocoa의 UITableView+Rx.swift를 살펴봅시다
public func items<Sequence: Swift.Sequence, Source: ObservableType>
(_ source: Source)
-> (_ cellFactory: @escaping (UITableView, Int, Sequence.Element) -> UITableViewCell)
-> Disposable
where Source.Element == Sequence {
return { cellFactory in
let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper<Sequence>(cellFactory: cellFactory)
return self.items(dataSource: dataSource)(source)
}
}
items라는 메서드가 존재함을 알 수 있습니다. 이를 활용하면 Cell을 구현할 수 있습니다. 구현 내용을 이해하는 것도 좋지만 우선 사용방법을 익히는 것은 목표로 하고 예제를 살펴봅시다.
let items = Observable.just([
"First Item",
"Second Item",
"Third Item"
])
items
.bind(to: tableView.rx.items) { (tableView, row, element) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "\(element) @ row \(row)"
return cell
}
.disposed(by: disposeBag)
조금 내려보면 이와 같은 형태의 주석처리된 샘플코드를 확인할 수 있습니다. 아주 좋은 사용 예제네요!
tableView.rx.items에 bind를 걸어주면 tableView, row, element로 구성된 클로저를 구현할 수 있게됩니다. (이 때 받아오는 event가 배열 한개를 통채로 넘겨주는 Observable<[String]> 타입임에 유의합시다! 배열을 기반으로 구성된 Observable<String>이 아닙니다!) 이를 통해 Cell을 구성하고 리턴하면 됩니다. Cell을 구성하는 것은 뭐 많이 해봤으니 익숙하겠죠? dequeueReusableCell로 Cell을 받아온 다음 element(Observable에서 방출된 item들)을 기반으로 Cell 형태를 구성하면 됩니다.
기존 UITableViewDataSource 객체의 cellForRowAt 메서드와 동일한 역할을 수행하고 있네요! 생각보다 쉽군요.
파일을 좀 더 내려보면 다른 샘플 코드를 하나 더 확인할 수 있습니다.
let items = Observable.just([
"First Item",
"Second Item",
"Third Item"
])
items
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
cell.textLabel?.text = "\(element) @ row \(row)"
}
.disposed(by: disposeBag)
이 역시 비슷한 방식인데요, 기본 Cell을 사용하지 않고 Custom Cell을 사용하는 경우에는 필수적으로 이 방법을 사용해야 합니다. 앞선 코드와 다르게 파라미터를 필요로하고 있죠? 여기에 Cell identifier와 Type을 입력해주면 됩니다.
이 메서드는 앞선 기본 items보다 더 많은 일을 수행해줍니다. 클로저를 보면 row, element, cell을 입력값으로 받죠? tableView가 빠진 대신 아예 직접 cell을 반환해주고 있는 것입니다. cell을 불러오는 작업도, 형 변환하는 과정도 내부적으로 처리해주기 때문에 사용자는 이를 받아와서 처리만해주면 됩니다.
RxCocoa - TableView 이벤트처리
기존에는 UITableViewDeleage를 활용해 이벤트를 처리해야 했습니다. 하지만 RxCocoa를 활용하면 Extension에 추가되어 있는 Observable을 구독하는 방식으로 이벤트를 처리할 수 있습니다.
이 역시 UITableView+Rx.swift를 살펴봅시다.
// events
/**
Reactive wrapper for `delegate` message `tableView:didSelectRowAtIndexPath:`.
*/
public var itemSelected: ControlEvent<IndexPath> {
let source = self.delegate.methodInvoked(#selector(UITableViewDelegate.tableView(_:didSelectRowAt:)))
.map { a in
return try castOrThrow(IndexPath.self, a[1])
}
return ControlEvent(events: source)
}
...
itemSelected를 포함해 다양한 ControlEvent가 존재함을 알 수 있습니다. itemSelected ControlEvent는 Cell이 선택될 때마다 didSelectRowAt message에 반응하여 Next로 이벤트를 방출합니다. 이벤트에는 IndexPath가 포함되어 있기 때문에 이를 활용한 작업을 처리할 수 있습니다.
결과적으로 UITableViewDelegate의 didSelectRowAt 메서드와 동일한 역할을 수행하네요!
tableView.rx.itemSelected
.subscribe(onNext: { [weak self] indexPath in
self?.tableView.deselectRow(at: indexPath, animated: true)
})
.disposed(by: disposeBag)
이를 활용해 이런 작업을 처리할 수 있습니다.
itemSelected를 활용해 item이 선택되었을 때 선택상태를 바로 해제하도록하는 예제입니다.
그런데 RxSwift에는 더 특이하고 유용한 메서드가 존재합니다.
public func modelSelected<T>(_ modelType: T.Type) -> ControlEvent<T> {
let source: Observable<T> = self.itemSelected.flatMap { [weak view = self.base as UITableView] indexPath -> Observable<T> in
guard let view = view else {
return Observable.empty()
}
return Observable.just(try view.rx.model(at: indexPath))
}
return ControlEvent(events: source)
}
modelSelected 메서드가 그것입니다. 이를 통하면 ModelType을 받아올 수 있습니다. (버퍼에 Observable로부터 받아온 데이터를 저장하고 있는 형태인 것 같네요)
tableView.rx.modelSelected(Item.self)
.subscribe(onNext: { item in
print(item.name)
})
.disposed(by: disposeBag)
이런식으로 구성하면 Item 타입에 존재하는 name값이 결과로 출력됩니다.
Observable.zip(tableView.rx.modelSelected(Item.self), tableView.rx.itemSelected)
.bind { [weak self] (item, indexPath) in
print(item.name)
self?.tableView.deselectRow(at: indexPath, animated: true)
}
disposed(by: disposeBag)
이 두개의 ControlEvent는 한번에 방출을 처리하기 때문에 zip으로 묶어 하나로 관리할 수 있습니다.
RxCocoa - delegate를 지정하는 방법
작업을 하다보면 RxCocoa의 ControlEvent와 더불어 기존 CocoaTouch 시스템의 Delegate를 활용해야 하는 경우가 많습니다.
tableView.delegate = self
그런데 막상 적용을 하고 사용하려하니 ControlEvent를 바인딩해 지정해준 작업 모두가 처리되지 않는 이슈를 만났습니다.
아마 delegate가 message를 가져가면서 RxCocoa 쪽에서는 확인할 수 없어져서 발생하는 문제인 것 같네요. 해결해봅시다.
tableView.rx.setDelegate(self)
.disposed(by: disposeBag)
Rx에서 해결방법을 자체적으로 제공합니다. rx가 제공하는 setDelegate 메서드를 사용하면 내부적으로 해당 에러를 처리해서 delegate를 지정해줍니다. 아주 편리하네요!
+) Section을 나누어 표시하고 삭제 및 이동을 구현하는 것은 RxCocoa에 존재하는 것만으로는 어렵습니다. RxCocoa를 덜어내고 Delegate만을 사용해 구현하거나, RxDataSource를 사용하는 것이 좋습니다. 다음번엔 이 RxDataSource에 대해 다뤄보겠습니다.
'🍎 Apple > Combine & Rx' 카테고리의 다른 글
[Combine] Publishing 타이밍 조절하기 (Connectable Publishers) (0) | 2023.10.27 |
---|---|
[RxSwift] Rx로 네트워크 통신하기 (0) | 2022.05.13 |
[RxSwift] Relay (PublishRelay, BehaviorRelay, ReplayRelay) (0) | 2022.02.25 |
[RxSwift] Driver, Signal (0) | 2022.02.24 |
[RxSwift] ControlProperty, ControlEvent (0) | 2022.02.24 |