본문 바로가기

iOS_RxSwift

RxSwift 를 통한 UITableView 사용법에 대한 정리

프로젝트를 진행하면서 네이티브 화면을 구성하며 참 많이 사용하는 것들이 UITableView, UICollectionView 들인것 같다.

 

하지만 아직 UITableView, UICollectionView 의 기본적인 사용법이야 알지만, 깊게 이해하고 있는것 같지가 않아,

정리를 하며 이해해나가고자 해당 포스팅을 계획하였다. 우선 UITableView 부터!

RxSwift 와 함께 사용했을때, 그리고 Section의 갯수를 1개로 고정 OR Section's Row 수를 1개로 고정할 경우 사용하기는 쉬우나,

Section의 갯수와 Section 별 Row 의 갯수가 유동적으로 변한다고 할때도 사용할 수 있어야 할것이다.

 

우선 SampleViewController 가 정의되어 있고, 해당 ViewController 에서 tableView를 하나 사용중이다.

그럼 다음과 같은 Delegate, DataSource 를 일반적(?), 필수적으로 정의할 것이다.

extension SampleViewController: UITableViewDelegate, UITableViewDataSource {
  func numberOfSections(in tableView: UITableView) -> Int {
    return 1 // optional 없으면 알아서 return 1
  }
  
  // 해당 섹션에 몇 개의 row를 표현해 주면 되니?
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 10
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // TODO: 
    return UITableViewCell()
  }
}

이것은 아주 간단한 사용법이고, 여기선 RxSwift, ReusableKit, RxDataSources를 이용하여 유동적으로 Section의 수와 Section별 row 의 수도 변하는 경우에 사용하는 예제를 정리하고 한다.

 

예제의 상황은 이러하다.

홍길동은 사과은행(Apple Bank)을 주거래 은행으로 사용하고 있고, 사과은행의 앱에서 오픈뱅킹을 등록하여 모든 금융기관의 계좌들을 한 앱에서 관리하고 있다.

사과은행의 전계좌조회 화면을 구성한다고 가정해보자.

import RxSwift
import UIKit
import ReusableKit
import RxDataSources
import RxRelay

final class AllAcctsViewController: BaseViewController {
  typealias DataSource = RxTableViewSectionedReloadDataSource<AllAcctsSection>
  
  private var dataSource: DataSource?
  private var acctsSection: [AllAcctsSection] = []
  
  init() {
    super.init()
    
    dataSource = DataSource(
      configureCell: { [weak self] _, tableView, indexPath, item -> UITableViewCell in
        
      switch item {
      case let .apple(acct):
        let cell = tableView.dequeue(Reusable.cell, for: indexPath)
        return cell

      case let .openBank(acct):
        let cell = tableView.dequeue(Reusable.cell, for: indexPath)
        return cell
        
      case .empty:
        let cell = tableView.dequeue(Reusable.empty, for: indexPath)
        return cell
      }
    },
    canEditRowAtIndexPath: { (_, _) in
      return true
    },
    canMoveRowAtIndexPath: { _, _ in
      return true
    })
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  enum Reusable {
    static let cell = ReusableCell<AllAcctsViewCell>(nibName: AllAcctsViewCell.nibName)
    static let empty = ReusableCell<AllAcctsEmptyViewCell>(nibName: AllAcctsEmptyViewCell.nibName)
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    tableView.do {
      $0.bounces = false
      $0.separatorStyle = .none
      $0.register(Reusable.cell)
      $0.register(Reusable.empty)
      $0.rx.setDelegate(self).disposed(by: disposeBag)
    }
    
    bindTableView()
  }
  
  private func bindTableView() {
    guard let dataSource = datsSource else { return }  
    let behaviorRelay = BehaviorRelay<[AllAcctsSection]>(value: self.acctsSection)
    behaviorRelay.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
  }
  
  @IBOutlet weak var tableView: UITableView!
}

자 RxTableViewSectionedReloadDataSource<AllAcctsSection> ⇒ 이 부분에 집중하자

open class RxTableViewSectionedReloadDataSource<Section: SectionModelType>
    : TableViewSectionedDataSource<Section>
    , RxTableViewDataSourceType {
    public typealias Element = [Section]

    open func tableView(_ tableView: UITableView, observedEvent: Event<Element>) {
        Binder(self) { dataSource, element in
            #if DEBUG
                dataSource._dataSourceBound = true
            #endif
            dataSource.setSections(element)
            tableView.reloadData()
        }.on(observedEvent)
    }
}

RxDataSources 의 RxTableViewSectionedReloadDataSource 클래스는 위와 같다.

우린 SectionModelType으로 AllAcctsSection을 쓰겠다는 것이다.

말 그대로 SectionModel 을 정의해서 이걸로 쓰겠어! 하여 AllAcctsSection을 정의하고,

tableView.rx.items 에 bind 하여야 한다. (bindTableView)

 

Observable을 변수 형태로 들고 다니며 사용하는 Relay, Subject 의 개념도 다시 한번 사용해보는 계기가 되었다 (BehaviorRelay)

하지만 Relay, Subject 의 개념은 더 깊숙히 알아봐야 할것 같다.. (내가 이해하고 있는 게 올바른 개념이 아닌 느낌이랄까?)

 

마지막으로 SectionModel 을 정의한 AllAcctsSection의 코드와 SectionModelType 의 정의에 대한 코드를 추가한다.

import Foundation

public protocol SectionModelType {
    associatedtype Item

    var items: [Item] { get }

    init(original: Self, items: [Item])
}

 

import RxDataSources

enum AllAcctsType {
  case apple
  case openBank
  case empty
}

enum AllAcctsItem {
  case apple(Account)
  case openBank(OpenBankAccount)
  case empty
}

struct AllAcctsSection {
  var kind: AllAcctsType
  var items: [AllAcctsItem]
}

extension AllAcctsSection: SectionModelType {
  typealias Item = AllAcctsItem

  init(original: AllAcctsSection, items: [Item]) {
    self = original
    self.items = items
  }
}

 

추후 tableView.rx.items 에 bind 하는 것이 현재 포스팅에 사용한

tableView.rx.items(dataSource: dataSource)) 말고도 몇가지가 더 있는데, 이에 대한 부분은

아래 주소를 참조하자!

출처: https://eunjin3786.tistory.com/29