MVVM
Swift
RxSwift
RxDataSources

RxDataSourcesライブラリ+UITableViewを使ってMVVMサンプルiOSアプリを作る

やること

  • RxDataSourcesライブラリを使ったMVVMサンプルアプリを作る
  • UITableViewを使ったサンプル

環境

  • macOS HighSierra
  • Xcode 9.4
  • iOS 11.4
  • Swift 4.1
  • cocoapods 1.5.3

完成イメージ

Simulator Screen Shot - iPhone 8 - 2018-07-14 at 02.25.19.png

repo

手順

環境構築

  • 新規プロジェクトをSingleViewAppで作成
  • ライブラリを導入する
vi Podfile
platform :ios, '11.4'
use_frameworks!

target 'RxDataSourceExample' do
  pod 'RxDataSources', '~> 3.0'
end
pod install

開発

Modelを作成する

  • 表示するデータを定義
Persion.swift
struct Person {
    let name: String
}
  • SectionModelを作成する
    • SectionModelTypeプロトコルに準拠する構造体でセクションを定義する
新しいクラスを作っても良いけど、自分はViewModelに書いたCustomViewModel.swift
struct SectionOfPerson {
    var header: String
    var items: [Item]
}

extension SectionOfPerson: SectionModelType {
    typealias Item = Person

    init(original: SectionOfPerson, items: [SectionOfPerson.Item]) {
        self = original
        self.items = items
    }
}
  • SectionOfPersonheader がSectionのTitleで、 items がSection内のセルデータ群

    • このサンプルアプリでは、headersection 1itemsPersion(name: “Nozaki”)などが入る
  • ViewController, ViewModelを作成

  • MVVMアーキテクチャ

  • ViewModelの作成

CustomViewModel.swift
class CustomViewModel {

    let items = PublishSubject<[SectionOfPerson]>()

    func updateItem() {
        var sections: [SectionOfPerson] = []
        sections.append(SectionOfPerson(header: "section 1", items: [SectionOfPerson.Item(name: "Nozaki"), SectionOfPerson.Item(name: "Sakura")]))
        sections.append(SectionOfPerson(header: "section 2", items: [SectionOfPerson.Item(name: "Kashima"), SectionOfPerson.Item(name: "Hori")]))
        sections.append(SectionOfPerson(header: "section 3", items: [SectionOfPerson.Item(name: "Seo"), SectionOfPerson.Item(name: "Wakamatsu")]))
        items.onNext(sections)
    }
}
CustomViewController.swift
import UIKit
import RxSwift
import RxDataSources

class CustomViewController: UIViewController, UITableViewDelegate {

    @IBOutlet weak var tableView: UITableView!

    private var disposeBag = DisposeBag()

    private lazy var dataSource = RxTableViewSectionedReloadDataSource<SectionOfPerson>(configureCell: configureCell, titleForHeaderInSection: titleForHeaderInSection)

    private lazy var configureCell: RxTableViewSectionedReloadDataSource<SectionOfPerson>.ConfigureCell = { [weak self] (dataSource, tableView, indexPath, person) in
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = person.name
        return cell
    }

    private lazy var titleForHeaderInSection: RxTableViewSectionedReloadDataSource<SectionOfPerson>.TitleForHeaderInSection = { [weak self] (dataSource, indexPath) in
        return dataSource.sectionModels[indexPath].header
    }

    private var viewModel: CustomViewModel!

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

extension CustomViewController {
    private func setupViewController() {
        self.title = "タイトル"
    }
    private func setupTableView() {
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.rx.setDelegate(self).disposed(by: disposeBag)
    }
    private func setupViewModel() {
        viewModel = CustomViewModel()

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

        viewModel.updateItem()
    }
}
  • 上記のコードで動くようになる、以下簡単なコメント
抜粋CustomViewController.swift
    # すごいやつ。
    # こいつとデータのPublishSubjectをbindすると`tableReload`や`numberOfSections`をよしなにやってくれる
    private lazy var dataSource = RxTableViewSectionedReloadDataSource<SectionOfPerson>(configureCell: configureCell, titleForHeaderInSection: titleForHeaderInSection)

    # delegateでいう `cellForRowAt` の部分
    private lazy var configureCell: RxTableViewSectionedReloadDataSource<SectionOfPerson>.ConfigureCell = { [weak self] (dataSource, tableView, indexPath, person) in
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = person.name
        return cell
    }

    # delegateでいう `titleForHeaderInSection` の部分
    private lazy var titleForHeaderInSection: RxTableViewSectionedReloadDataSource<SectionOfPerson>.TitleForHeaderInSection = { [weak self] (dataSource, indexPath) in
        return dataSource.sectionModels[indexPath].header
    }

    // ~~~~~~
    # SectionModelとdataSourceをbindさせる
    viewModel.items
        .bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)

わかった

  • MVVMとめちゃ相性良い
  • 導入コスト高い
    • RxSwift初心者(1ヶ月ほど)の自分でもこのサンプルアプリを作るのに4.5Hくらいかかった
  • そこまでごりごりUITableViewを使うようなアプリじゃないなら、わざわざ導入するまでもなさそう
    • 普通のDelegateメソッド使ったほうが早い (慣れだけど)
  • 今回は標準のRxTableViewSectionedReloadDataSource を使ったが、データソースが変更された場合に自動的にアニメーションしながら更新してくれる Animated Data Sources という仕組みもある模様 =>
    • 少し書き方を変更しなければいけないけど

次にやる

  • 開発中の個人プロダクトにも入れてみるぞ 💪
  • 気が向いたらAnimated Data Sourcesを触ってみるぞ!
  • UICollectionView の場合の記事も書きたい