swift初心者がSmartNews風ニュースアプリを作ってみる過程を晒す(8) - RxSwiftを用いてMVVMを実装する


はじめに

swiftはほとんど未経験ですが、SmartNews風ニュースアプリを作ってみて、その過程をさらしています。

前回は、こんな記事を書きました。

swift初心者がSmartNews風ニュースアプリを作ってみる過程を晒す(7) - MVVMで関心事を分離する手法を学ぶ - Qiita

今回は、RxSwiftを用いて実際にMVVMアーキテクチャを使用した実装を行います。

RxSwiftを使用したMVVM

取得した記事を表示するシンプルなニュースアプリを作成します。
ニュース記事のデータは、ここから取得します。

Model

Entryモデルは一つ一つの記事を表現します。
JSONオブジェクトをparseし、各プロパティにセットすることで初期化されます。

Entry.swift
import Foundation
import RealmSwift

class Entry: Object {

    dynamic var title: String = ""
    dynamic var link: String = ""
    dynamic var contentSnippet: String = ""
}

ViewModel

ViewModelには以下の責務をもたせます。

  • ビジネスロジック(ネットワークリクエストを発生させるetc)
  • 表示に必要なデータをViewControllerに届ける
  • Modelの更新を監視する
EntriesViewModel.swift
import RxSwift
import RxCocoa

final class EntriesViewModel {

    //MARK: - Dependecies

    //MARK: - Model

    var entries: Driver<[Entry]> = Driver.never()

    //MARK: - Set up

    init() {
        //Initialise dependencies

    }

    func reloadData(title: String) {
        entries = EntryAPIService().fetchEntries(q: title)
    }
}

DriverはUIレイヤーを直感的にリアクティブプログラミングするための部品です。
Driverって何なんだ、ということに関しては、こちらに詳しく記載されています。

Units are totally optional. You can use raw observable sequences everywhere in your program and all RxCocoa APIs work with observable sequences.

公式ドキュメントにも記載があるように、使用しないと実装できないものでもないですが、便利なので使いましょう、という位置付けのようです。

Driverの特徴

Driverの特徴は以下の通りです。

  • エラーでストリームが終了しない
    • JSONのparseエラーやAPIとの通信失敗が起きた場合に、UIコンポーネントが更新できなくなるようなことを防ぎます
  • main schedulerでsubscribe, observeする
    • API通信の結果を使用して、バックグラウンドスレッドでUIコンポーネントを更新するようなことをするとCrashの原因になります
  • 副作用をシェアする
    • API通信の結果を複数のUIコンポーネントにバインドした時に、2回HTTPリクエストが発生するような事を回避します

公式ドキュメントでは、典型的なサンプルコードを交えて非常に詳しく解説されているので、一読をお勧めします

ViewController

TableViewController.swift

   @IBOutlet weak var tableView: UITableView!

    private let bag = DisposeBag()
    private var entryList: [Entry]  = []

    // create ViewModel
    let viewModel = EntriesViewModel()

    override func viewDidLoad() {

        super.viewDidLoad()
        self.tableView.registerNib(UINib(nibName: "EntryTableViewCell", bundle: nil), forCellReuseIdentifier: "EntryTableViewCell")

        viewModel.reloadData(self.title!) //1

        // see http://yannickloriot.com/2016/01/make-uitableview-reactive-with-rxswift/
        // bind articles to UITableView

        // If there is a `drive` method available instead of `bindTo`,
        // that means that the compiler has proven that all properties
        // are satisfied.
        viewModel.entries.drive(self.tableView.rx_itemsWithCellIdentifier("EntryTableViewCell")) { //2
            (index, entry: Entry, cell:EntryTableViewCell) in
            cell.updateCell(entry)
        }.addDisposableTo(bag) //3

        viewModel.entries.driveNext { [unowned self] in //4
            self.entryList = $0
        }.addDisposableTo(bag)

    }
  • 1. APIにアクセスして記事の取得を行っています。
  • 2. entriesの更新をトリガーにして、tableviewの更新を行っています。
  • 3. unsubscribeを自動的に行って、リソースを解放するための記述です。

Using dispose bags or takeUntil operator is a robust way of making sure resources are cleaned up. We recommend using them in production even if the sequences will terminate in finite time.

感想

  • 各クラスの責務が明確になり、仕様変追加時の変更箇所が明確になりました。
  • テストが書きやすくなりそうです。

おわりに

ソースコードはこちらです。

次回は、少し趣を変えて、実装したアプリケーションのテストコードを書いていきます。

参考