[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/Combine & Rx

[RxSwift] RxCocoa로 TableView 구현하기

inu 2022. 5. 2. 23:21
반응형

안녕하세요 이누입니다.

오늘은 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에 대해 다뤄보겠습니다.

반응형