천원의 개발

Swift Combine을 활용하여 간단하게 tableView를 그려보자 본문

iOS&Swift🍎/Combine

Swift Combine을 활용하여 간단하게 tableView를 그려보자

천 원 2025. 4. 8. 10:49

안녕하세요 천원입니다.

오늘은 Combine을 사용하면서 RxCocoa의 bind 메서드처럼 간편하게 UITableView를 그릴 수 없을까 고민한 내용을 공유드립니다.

 

1. RxSwift TableView 그리기

let items = Observable.just([
             "First Item",
             "Second Item",
             "Third Item"
         ])

/// bind 메서드를 활용한 tableView  
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)

 

위의 코드와 같이 bind 메서드를 활용해서 간편히 tableView를 그릴 수 있습니다.


2. Combine TableView 그리기

기존에는 UITableViewDataSource를 활용하여 tableView를 구현하였습니다.

// Return the number of rows for the table.     
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return 0
}


// Provide a cell object for each row.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   // Fetch a cell of the appropriate type.
   let cell = tableView.dequeueReusableCell(withIdentifier: "cellTypeIdentifier", for: indexPath)
   
   // Configure the cell’s contents.
   cell.textLabel!.text = "Cell text"
       
   return cell
}


그러나 최근에는 Combine과 함께는 UITableViewDiffableDataSource, NSDiffableDataSourceSnapshot 를 활용하여 코드를 작성하고 있습니다. 자세한 내용은 https://developer.apple.com/videos/play/wwdc2019/220 해당 wwdc 영상을 통하여 확인이 가능합니다.

private func setupDataSource() {
    dataSource = UITableViewDiffableDataSource<Int, String>(tableView: tableView) { tableView, indexPath, itemIdentifier in
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = itemIdentifier
        return cell
    }
}

private func applySnapshot(with items: [String]) {
    var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
    snapshot.appendSections([0])
    snapshot.appendItems(items)
    dataSource.apply(snapshot, animatingDifferences: true)
}

 

이런식으로 매번 tableView를 구현 시 dataSource와 snapshot을 구현해 적용해 줘야하는 불편함이 존재합니다.

 

 

3. Combine + UITableViewDiffableDataSource

반복적으로 작성하는 dataSource와 snapShot을 한번에 만들어 줄 메서드를 구현해 봅시다. 

 

우선 DataSoucre 작성을 위해 AnyPublisher 타입의 데이터 리스트를 제네릭하게 받습니다(AnyPublisher<[T], Never>)
또한 초기화 시 필요한 cellProvider 클로저 또한 받아서 DataSource를 구현해 줍니다. 

func bind<T: Hashable>(
    to publisher: AnyPublisher<[T], Never>,
    cellProvider: @escaping (UITableView, IndexPath, T) -> UITableViewCell
) {
    let dataSource = UITableViewDiffableDataSource<Int, T>(tableView: self) { tableView, indexPath, item in
        return cellProvider(tableView, indexPath, item)
    }
}

 


그런 후 SnapShot을 만들기 위해 item 리스트를 받아와 줘야하니 publisher의 sink 메서드를 통해서 [T] 리스트를 받아와 snapShot을 만들어 줍시다.

 

func bind<T: Hashable>(
    to publisher: AnyPublisher<[T], Never>,
    cellProvider: @escaping (UITableView, IndexPath, T) -> UITableViewCell
) {
    let dataSource = UITableViewDiffableDataSource<Int, T>(tableView: self) { tableView, indexPath, item in
        return cellProvider(tableView, indexPath, item)
    }

    publisher
        .sink { items in
            var snapshot = NSDiffableDataSourceSnapshot<Int, T>()
            snapshot.appendSections([0])
            snapshot.appendItems(items)
            dataSource.apply(snapshot, animatingDifferences: true)
        }
}

 


마지막으로 여러 곳에서 편하게 사용할 수 있도록 해당 메서드를 extension으로 적용해 두면,

extension UITableView {
    func bind<T: Hashable>(
        to publisher: AnyPublisher<[T], Never>,
        cellProvider: @escaping (UITableView, IndexPath, T) -> UITableViewCell
    ) -> AnyCancellable {
        let dataSource = UITableViewDiffableDataSource<Int, T>(tableView: self) { tableView, indexPath, item in
            return cellProvider(tableView, indexPath, item)
        }
        
        return publisher
            .sink { items in
                var snapshot = NSDiffableDataSourceSnapshot<Int, T>()
                snapshot.appendSections([0])
                snapshot.appendItems(items)
                dataSource.apply(snapshot, animatingDifferences: true)
            }
    }
}

/// 사용하는 쪽
tableView.bind(to: transaction.eraseToAnyPublisher()) { tableView, indexPath, item in
    let cell = tableView.dequeueReusableCell(withIdentifier: DespositCell.identifier, for: indexPath) as! DespositCell
    cell.bind(item)
    return cell
}
.store(in: &cancellables)


간편하게 Combine을 활용한 간편한 테이블 뷰 구현이 완성됩니다. 🎉