📖

【学習備忘録】RxDataSourcesを使ってリスト表示をする

2022/05/20に公開

はじめに

「比較して学ぶ RxSwift4 入門」のCounterアプリのコードをもとに学習しました。
RxDataSourcesを使ってUITableViewアプリを実装します。

サンプルコード

完成

1. SectionModel

  • SettingsSectionでセクション数・headerとfooterの高さを決める。
  • SettingsItemでセルに表示する内容を定義する。
  • SectionModel<SettingsSection, SettingsItem>をtypealiasでSettingsSectionModelの型として定義する。
typealias SettingsSectionModel = SectionModel<SettingsSection, SettingsItem>

enum SettingsSection {
    case account
    case common

    var headerHeight: CGFloat {
        return 40.0
    }

    var footerHeight: CGFloat {
        return 1.0
    }
}

enum SettingsItem {
    // account section
    case account
    case security
    case notification
    case contents
    // common section
    case sounds
    case dataUsing
    case accessibility

    // other
    case description(text: String)

    var title: String? {
        switch self {
        case .account:
            return "アカウント"
        case .security:
            return "セキュリティ"
        case .notification:
            return "通知"
        case .contents:
            return "コンテンツ設定"
        case .sounds:
            return "サウンド設定"
        case .dataUsing:
            return "データ利用時の設定"
        case .accessibility:
            return "アクセシビリティ"
        case .description:
            return nil
        }
    }

    var rowHeight: CGFloat {
        switch self {
        case .description:
            return 72.0
        default:
            return 48.0
        }
    }

    var accessoryType: UITableViewCell.AccessoryType {
        switch self {
        case .account, .security, .notification, .contents, .sounds, .dataUsing, .accessibility:
            return .disclosureIndicator
        case .description:
            return .none
        }
    }
}

2. SettingsViewModel

  • 定数のitemは、.errorや.completedが流れてこないようBehaviorRelayで.nextだけ流せるようにする。.nextイベントを流すにはacceptメソッドを使う。
  • accountSectionとcommonSectionの内容をupdateItems()でまとめる。
class SettingsViewModel {

    let items = BehaviorRelay<[SettingsSectionModel]>(value: [])

    var itemObsevable: Observable<[SettingsSectionModel]> {
        return items.asObservable()
    }

    func setup() {
        updateItems()
    }

    func updateItems() {
        let sections: [SettingsSectionModel] = [
            accountSection(),
            commonSection()
        ]
        items.accept(sections)
    }

    private func accountSection() -> SettingsSectionModel {
        let items: [SettingsItem] = [
            .account,
            .security,
            .notification,
            .contents
        ]
        return SettingsSectionModel(model: .account, items: items)
    }

    private func commonSection() -> SettingsSectionModel {
        let items: [SettingsItem] = [
            .sounds,
            .dataUsing,
            .accessibility,
            .description(text: "基本設定はこの端末でログインしている全てのアカウントに適用されます。")
        ]
        return SettingsSectionModel(model: .common, items: items)
    }
}

3. ViewController

  • @IBOutletでUITableViewを接続。
  • セルの設定を変数のconfigureCellで行う。
  • 設定したセルをdataSourceにセットする。
    • lazy(遅延格納プロパティ)は、 利用するまで初期化処理を走らせないようにする。
  • setupTableView()でtableViewの設定をする。
    • cellの登録。
    • contentInsetの設定。
    • delegateで委任し、extentionでセルの高さなどを指定。
    • タップ時の処理の設定。
  • ViewModelの設定。
    • ViewModelのitemsとtableViewのitemsを.bindする。
    • updateItems()でデータを流す。
class SettingsViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    private var disposeBag = DisposeBag()

    private lazy var dataSource = RxTableViewSectionedReloadDataSource<SettingsSectionModel>(configureCell: configureCell)

    private lazy var configureCell: RxTableViewSectionedReloadDataSource<SettingsSectionModel>.ConfigureCell = {
        [weak self] (dataSource, tableView, indexPath, _) in
        let item = dataSource[indexPath]
        switch item {
        case .account, .security, .notification, .contents, .sounds, .dataUsing, .accessibility:
            let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
            cell.textLabel?.text = item.title
            cell.accessoryType = item.accessoryType
            return cell
        case .description(let text):
            let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
            cell.textLabel?.text = text
            cell.isUserInteractionEnabled = false
            return cell
        }
    }

    private var viewModel: SettingsViewModel!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        setupViewModel()
    }

    private func setupTableView() {
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.contentInset.bottom = 12.0
        tableView.rx.setDelegate(self).disposed(by: disposeBag)
        tableView.rx.itemSelected
            .subscribe(onNext: { [weak self] indexPath in
                guard let item = self?.dataSource[indexPath] else { return }
                self?.tableView.deselectRow(at: indexPath, animated: true)
                switch item {
                    case .account:
                    // 遷移させる処理
                    break
                case .security:
                    break
                case .notification:
                    break
                case .contents:
                    break
                case .sounds:
                    break
                case .dataUsing:
                    break
                case .accessibility:
                    break
                case .description:
                    break
                }
            })
            .disposed(by: disposeBag)
    }

    private func setupViewModel() {
        viewModel = SettingsViewModel()

        viewModel.items
            .bind(to: tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)

        viewModel.updateItems()
    }
}

extension SettingsViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let item = dataSource[indexPath]
        return item.rowHeight
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        let section = dataSource[section]
        return section.model.headerHeight
    }

    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        let section = dataSource[section]
        return section.model.footerHeight
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerView = UIView()
        headerView.backgroundColor = .clear
        return headerView
    }

    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        let footerView = UIView()
        footerView.backgroundColor = .clear
        return footerView
    }
}

GitHub

https://github.com/MasakatsuTagishi/Practice-RxDataSource

参考にしたもの

1.比較して学ぶRxSwift入門

https://www.amazon.co.jp/比較して学ぶRxSwift入門-技術の泉シリーズ(NextPublishing)-髙橋-凌-ebook/dp/B07LGMNXS3

さいごに

いやー、難しい...
処理を追いながら、説明を書き加えた(全く理解できない説明になっていると思います...)のですが、これを使いこなせる自信がないので、もっと精進します。
間違いや認識違いがあれば指摘いただければ幸いです。

Discussion