본문 바로가기
Swift/UIKit

RxSwift와 Diffable DataSource에서의 ModelSelected 구현

by songmoro 2025. 9. 10.

서론

Rx에서는 선택한 셀의 IndexPath를 반환하는 ItemSelected, 선택한 셀의 IndexPath를 기반으로 데이터 모델을 반환하는 ModelSelected 메서드를 제공한다.

 

ItemSelected는 collectionView(:didSelectItemAtIndexPath:), tableView(:didSelectRowAtIndexPath:)를 delegate를 래핑 한 메서드이고, ModelSelected는 그러한 ItemSelected를 한 번 더 래핑 한 메서드이다.

 

 

이 두 메서드를 사용해서 뷰 컨트롤러에 Delegate를 채택하지 않아도, 셀이 선택되면 해당 셀에 대한 정보(인덱스패스, 데이터)를 조작할 수 있게 된다.

 

기본적인 itemSelected, modelSelected 사용법

 

아래는 배경색을 지닌 셀을 보여주는 간단한 코드


import UIKit
import SnapKit
import RxSwift
import RxCocoa

final class Cell: UITableViewCell {
    static let identifier = "Cell"
}

final class ViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private let tableView = UITableView()
    private let data: [UIColor] = [.systemRed, .systemPink, .systemOrange, .systemBlue]

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground

        view.addSubview(tableView)

        tableView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }

        tableView.register(Cell.self, forCellReuseIdentifier: Cell.identifier)

        Observable.just(data)
            .bind(to: tableView.rx.items(cellIdentifier: Cell.identifier, cellType: Cell.self)) { row, item, cell in
                cell.contentView.backgroundColor = item
            }
            .disposed(by: disposeBag)
    }
}

 

 

 

 

셀 클릭 시 아래와 같이 셀에 대한 인덱스패스와 색이 출력된다.

tableView.rx.itemSelected
    .bind {
        print("indexPath:", $0)
    }
    .disposed(by: disposeBag)

tableView.rx.modelSelected(UIColor.self)
    .bind {
        print("UIColor:", $0)
    }
    .disposed(by: disposeBag)
indexPath: [0, 0]
UIColor: <UIDynamicCatalogSystemColor: 0x600001740300; name = systemRedColor>

 

Diffable DataSource

위 코드를 디퍼블 데이터소스를 사용하는 코드로 바꾸고, 셀을 클릭한다면?

 

dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, itemIdentifier in
    let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier, for: indexPath)
    cell.contentView.backgroundColor = itemIdentifier
    return cell
}

tableView.dataSource = dataSource

var snapshot = NSDiffableDataSourceSnapshot<Int, UIColor>()
snapshot.appendSections([0])
snapshot.appendItems(data)

dataSource.apply(snapshot)

 

modelSelected로 인해서 fatalError가 발생한다.

 

 

modelSelected와 Diffable DataSource의 한계점

디퍼블 데이터소스는 인덱스패스가 아닌 스냅샷을 기준으로 동작한다.

Rx에서 modelSelected는 인덱스패스 접근 기반으로 구현되어 있기 때문에 디퍼블 데이터소스 방식과 호환되지 않아 fatalError를 반환하는 것

 

 

그렇기 때문에 "다른 화면에 값을 전달한다." 등의 동작을 위해 itemSelected로 인덱스패스를 받고, 이를 통해 데이터의 인덱스에 접근하는 식의 데이터를 찾아가는 식으로 사용했으며, 이를 좀 더 효율적으로 하기 위해 확장했다.

 

 

구현 아이디어

  1. itemSelected를 통해 IndexPath를 얻을 수 있음
  2. 데이터소스에 IndexPath를 인자로 아이템을 반환하는 메서드가 있음
    func itemIdentifier(for indexPath: IndexPath) -> Any?

 

이 둘을 통해 테이블 뷰 혹은 컬렉션 뷰에서 아이템의 인덱스패스를 받고, 데이터소스에서 해당 인덱스패스의 아이템을 찾아서 반환하기.

 

구현 코드

import UIKit
import RxSwift
import RxCocoa

extension Reactive where Base: UITableView {
    func sectionSelected<SectionIdentifierType, ItemIdentifierType>(_ dataSource: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>) -> ControlEvent<SectionIdentifierType> where SectionIdentifierType: Hashable, ItemIdentifierType: Hashable {
        let source: Observable<SectionIdentifierType> = self.itemSelected
            .compactMap { [weak dataSource] indexPath in
                dataSource?.sectionIdentifier(for: indexPath.section)
            }

        return ControlEvent(events: source)
    }

    func itemSelected<SectionIdentifierType, ItemIdentifierType>(_ dataSource: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>) -> ControlEvent<ItemIdentifierType> where SectionIdentifierType: Hashable, ItemIdentifierType: Hashable {
        let source: Observable<ItemIdentifierType> = self.itemSelected
            .compactMap { [weak dataSource] indexPath in
                dataSource?.itemIdentifier(for: indexPath)
            }

        return ControlEvent(events: source)
    }
}

extension Reactive where Base: UICollectionView {
    func sectionSelected<SectionIdentifierType, ItemIdentifierType>(_ dataSource: UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>) -> ControlEvent<SectionIdentifierType> where SectionIdentifierType: Hashable, ItemIdentifierType: Hashable {
        let source: Observable<SectionIdentifierType> = self.itemSelected
            .compactMap { [weak dataSource] indexPath in
                dataSource?.sectionIdentifier(for: indexPath.section)
            }

        return ControlEvent(events: source)
    }

    func itemSelected<SectionIdentifierType, ItemIdentifierType>(_ dataSource: UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>) -> ControlEvent<ItemIdentifierType> where SectionIdentifierType: Hashable, ItemIdentifierType: Hashable {
        let source: Observable<ItemIdentifierType> = self.itemSelected
            .compactMap { [weak dataSource] indexPath in
                dataSource?.itemIdentifier(for: indexPath)
            }

        return ControlEvent(events: source)
    }
}

 

사용 예

tableView.rx.itemSelected(dataSource)
    .bind {
        print("UIColor:", $0)
    }
    .disposed(by: disposeBag)
indexPath: [0, 0]
UIColor: <UIDynamicCatalogSystemColor: 0x600001741fc0; name = systemRedColor>

 

전, 후 코드 비교

/// 전
tableView.rx.itemSelected
    .compactMap { [weak self] indexPath in
        guard let self else { return nil }
        return self.data[indexPath.row]
    }
    .bind {
        print("UIColor:", $0)
    }
    .disposed(by: disposeBag)

/// 후
tableView.rx.itemSelected(dataSource)
    .bind {
        print("UIColor:", $0)
    }
    .disposed(by: disposeBag)
UIColor: <UIDynamicCatalogSystemColor: 0x600001712180; name = systemRedColor>
UIColor: <UIDynamicCatalogSystemColor: 0x600001712180; name = systemRedColor>

 

참고

https://developer.apple.com/documentation/uikit/uitableviewdelegate/tableview(_:didselectrowat:)
https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/collectionview(_:didselectitemat:)?language=objc
https://developer.apple.com/documentation/uikit/uicollectionviewdiffabledatasourcereference/itemidentifier(for:)