こんにちは。
現在、iOSとAndroidの開発を行なっている高橋洸介(@KoH_1011)です。
Wowma!アプリではMVVMのアーキテクチャを使用しています。
ただ、最近Fluxもいいよ!という声をよく聞くので、現在採用しているMVVMとFluxを合わせた実装を試してみたいと思います。
この記事を読むことで以下のことがわかる。というのを目標に書いていきます。
・Fluxについてざっくり理解できる
・Fluxのコードのイメージができる
※KDDIコマースフォワード㈱ 、略称「KCF」は2019年4月1日、同グループ会社の㈱ルクサと合併し「auコマース&ライフ株式会社」として再設立いたしました。 本記事は2019年3月31日以前に書かれた記事のアーカイブとなります。予めご了承ください。
Fluxとは
FluxとはFacebookが提唱したUIを持ったアプリを作るためのアーキテクチャです。
公式では以下のように説明されています。
Flux is a pattern for managing data flow in your application. The most important concept is that data flows in one direction. As we go through this guide we'll talk about the different pieces of a Flux application and show how they form unidirectional cycles that data can flow through. Fluxは、アプリケーションのデータフローを管理するためのパターンです。最も重要な概念は、データが一方向に流れることです。このガイドでは、Fluxアプリケーションのさまざまな部分について説明し、データが流れることができる単方向サイクルをどのように形成するかを示します。
Fluxは以下の図でよく説明されています。
図でわかるようにデータが単一方向に流れるので非常に見通しがよくなるほか、「データはすべて Action
を介して更新する」という制約があるため、 View
の更新状態を予測しやすくなります。
それぞれの責務を説明すると、
・Dispatcher:発火された Action
を Store
に通知するもの
・Store:dispatcher
経由できた Action
を格納するもの
・Action:View
等から発火されたイベントを dispatcher
に通知するもの
・View(ViewController): Store
のデータを反映するもの。イベントを Action
に通知するもの
開発環境
・Xcode:9.4
・Swift:4.1
仕様
QiitaAPIを使って以下のことを実現させていきます。
・ViewController
に記事がリスト表示されている。
・DetailViewController
にお気に入り状態が表示されている。
・ViewController
と DetailViewController
のお気に入り状態が同期されている。
使用するライブラリ
まずは使用するライブラリを取り込みます。
使用するライブラリは実際にWowma!アプリでも使用している以下のものを使っていきます。
・APIKit
・Himotoki
・RxSwift
Model
Himotoki
を使って Model
の生成をしていきます。
今回はお気に入り状態の更新を見ていきたいので、 title
と favorite
の2つを定義しています。
Request
APIKit
を使って Request
の生成をしていきます。
リクエスト先は (https://qiita.com/api/v2) になります。
このAPIで新着記事一覧を取得できます。
Flux
Fluxには以下の要素が必要なので、実装していきます。
・Action
・Dispatcher
・Store
Action
まずは Action
の実装をしていきます。
今回の仕様は
・
ViewController
に記事がリスト表示されている。・
DetailViewController
にお気に入り状態が表示されている。・
ViewController
とDetailViewController
のお気に入り状態が同期されている。
なので、 Action
で実装するイベントは以下の2つになります。
① 一覧の取得
② お気に入り状態の更新
まずは
① 一覧の取得
①一覧の取得
では、実際にリクエストを投げてそのレスポンス [Article]
を dispatch
します。
エラーの場合も dispatch
する必要がありますが、今回は割愛します。
dispatch
の処理は後ほど Dispatcher
の箇所で実装していきます。
実際にリクエストを投げる処理を実装していきます。
func load() { let request = ArticleRequest() Session.send(request) { (result: Result<ArticleRequest.Response, SessionTaskError>) in switch result { case .success(let articles): // 一覧の取得 case .failure(let error): // エラー } } }
上記の処理でレスポンス [Article]
を取得できました。
次に取得したレスポンス [Article]
を実際に dispatch
していきます。
後ほど実装しますが、 dispatcher
が必要になるので、初期化メソッドで dispatcher
を生成します。
private let dispatcher: ArticleDispatcher init(dispatcher: ArticleDispatcher = .shared) { self.dispatcher = dispatcher }
あとは後ほど Dispatcher
で実装する dispatch
メソッドに値を渡すだけです。
self.dispatcher.dispatch(obj: articles)
② お気に入り状態の更新
大枠は上記の実装で終えているので、 dispatch
する関数だけ追加します。
お気に入り状態の更新で必要な情報は Article
なのでこれを引数にして dispatch
します。
こんな感じ。
func update(article: Article) { self.dispatcher.dispatch(obj: article) }
以上で Action
の実装は終わりです。
Dispatcher
続いて Dispatcher
を実装していきます。
Action
から来るイベントは以下の2つになるので、①の関数と②の関数を実装していきます。
① 一覧の取得
② お気に入り状態の更新
先ほど Action
の実装で出てきた dispatch
をここで実装します。
func dispatch(obj: [Article]) { self.articles.onNext(obj) } func dispatch(obj: Article) { self.article.onNext(obj) }
dispatch
する関数ができたので、これを Store
に通知する仕組みを実装していきます。今回は通知する仕組みに PublishSubject
を使用したいと思います。こんな感じですね。
let articles = PublishSubject<[Article]>() let article = PublishSubject<Article>()
また、複数インスタンスだとデータを受け取った受け取ってないということが起きてしまうので、 singleton
で実装します。
static let shared = ArticleDispatcher()
Store
続いて Store
を実装していきます。
Store
で必要な情報は Dispatcher
で dispatch
した PublishSubject
を購読して処理に移すことです。
また、購読した値を View
に反映させる責務もあるのでその実装もしていきます。
購読するものは Dispatcher
で実装した articles
と article
になるので、まずは初期化メソッドにて dispatcher
を受け取ります。
required init(dispatcher: ArticleDispatcher = .shared) { super.init(dispatcher: dispatcher) }
dispatcher
を受け取ったらそれをもとに購読をしていきます。
/// dispatcherのarticlesの購読 dispatcher.articles.subscribe(onNext: { [weak self] (articles) in // 処理 }).disposed(by: disposeBag) /// dispatcherのarticleの購読 dispatcher.article.subscribe(onNext: { [weak self] (article) in // 処理 }).disposed(by: disposeBag)
次に購読したあとの処理を実装していきます。
必要な実装としては以下になります。
・articlesの更新処理
・お気に入り状態を更新する処理
更新処理の実装をする前に View
の更新に必要な articles
と article
を定義します。
BehaviorRelay
についてはこちらの記事に概要が記載されています。
private(set) var articles = BehaviorRelay<[Article]>(value: []) private(set) var article = BehaviorRelay<Article>(value: Article())
定義をしたら実際の更新処理を書いていきます。
articles
の更新は一覧の更新になるので、値を特に変更せずにそのまま更新します。
self?.articles.accept(articles)
article
の更新はお気に入り状態の更新になるので、詳細用の article
と 一覧用の articles
の両方を更新する必要があります。
また、更新する際にお気に入り状態も更新します。
お気に入り状態を更新する際は ID
等で比較して更新するのがベターですが、今回は比較できるものが title
しかないので、 title
の一致でお気に入り状態を更新します。
guard var articles = self?.articles.value else { return } articles.enumerated().forEach { (index, value) in if value.title == article.title { articles[index].favorite = !articles[index].favorite self?.article.accept(articles[index]) } } self?.articles.accept(articles)
以上で Store
の実装は終わりです。
TopView
Flux
の肝となる部分の実装が終わったので、 Flux
を用いながら MVVM
の実装をしていきます。
まずはトップの記事一覧で使う ViewModel
を実装します。
ViewModel
では Action
の実行と Store
を購読します。
Action
の実行は簡単ですね。
そのまま呼ぶだけです。
func load() { self.action.load() } func update(article: Article) { self.action.update(article: article) }
Store
の購読も簡単ですね。
こんな感じです。
self.store.articles .asObservable() .bind(to: _articles) .disposed(by: disposeBag)
bind
してる先は BehaviorRelay
を定義してあります。
private(set) var articles = BehaviorRelay<[Article]>(value: [])
ここまでの流れをみるとやりたいことは1つですね。
articles
を TopViewController
で購読させます。
ViewController
では以下のように購読させて自前で作成した TopDataSource
に bind
します。
長くなりましたが、これで記事の一覧を表示することができました。
お気に入りもできます。
viewModel.articles .asObservable() .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag)
記事一覧
DetailView
記事の一覧の実装が終わったので、あとは詳細画面の実装をしていきます。
Flux
の部分は先ほどの実装で終わってるので詳細用の ViewModel
を実装します。
先ほどの ViewModel
の実装と同じように Action
の実行と Store
を購読していきます。
Action
の実行は例によって実行するだけになります。
func update(article: Article) { self.action.update(article: article) }
Store
の購読も先ほどとほぼ同じです。
self.store.article .asObservable() .bind(to: article) .disposed(by: disposeBag)
ここでも bind
をしていますが、 bind
している先は先ほどと同じ BehaviorRelay
です。
private(set) var article = BehaviorRelay<Article>(value: Article())
ここも上と同じですね。
article
を DetailViewController
に購読させます。
DetailViewController
では以下のように購読させて自身の article
に bind
します。
これで詳細画面を表示することができました。
詳細
お気に入りの同期
TopViewController
で お気に入りした情報
は DetailViewController
でも反映されます。
ただ、このままだと DetailViewController
で変更した お気に入り情報
は TopViewController
に反映されません。
なので、 DetailViewController
の お気に入り情報
の変更を先ほど ViewModel
で実装した update
に投げてみましょう。
まずは、お気に入りボタンのイベントを取得する必要があるので、以下のように subscribe
します。
favoriteButton.rx.tap .subscribe { [weak self] _ in // 処理 }.disposed(by: disposeBag)
これで subscribe
できたので、あとは ViewModel
の update
を呼ぶだけです。
guard let article = self?.viewModel.article.value else { return } self?.viewModel.update(article: article)
これで、お気に入り状態は TopViewController
と DetailViewController
で同期されるようになりました。
トップから詳細へ遷移
詳細からトップへ戻る
やってみて
MVVM+Fluxのメリット
・データが一方向にしか流れない点
・各 class
の責務が明確なので複数人で実装をしてもそこまでブレない
MVVM+Fluxのデメリット
・Rxの知識が必要
・習得までのハードル
最後に
Wowma!のアプリ開発では Flux
を採用していませんが、これを機に少しづつ採用してもいいかもと思いました。
反響があれば今回実装したリポジトリを別途公開しようと思います。
次回は Flux
のライブラリの ReactorKit
について書きたいと思います。