🐷

SwiftUIでMVPのiOSアプリをつくってみた

2021/09/27に公開

前回の続きです。
が、独立して読めるように書きます。

# 前回の記事
https://zenn.dev/st43/articles/3be5f1057c5141

参考書

アーキテクチャーに関する知識は、こちら↓を参照しています。
以下の文中で「参考書」と出てきたらこの本のことを指します。

https://peaks.cc/books/iOS_architecture

環境

  • Xcode: 13.0
  • iOS: 15.0
  • Swift: 5.5

MVPとは

  • MVCのControllerの役割を厳密化したアーキテクチャー
  • PresenterによってViewのプレゼンテーションロジックを担当させる
  • Modelの状態変更の通知方法で2パターンあり、メリデメあるが、MVP(Passive View)が基本的には良さそう。テスト容易性が高いので

歴史

  • 1996: Taligent(Apple + IBM)のMike Potelによって論文で発表
    • このMVPは責務を細分化したものであり、大まかなコンセプトのみが後世に継承された
  • 2000: Dolphin Smalltalk(Windowsに進出したSmalltalk)にMVP(Supervising Controller)が採用
  • 2004: Martin FowlerがMVP(Supervising Controller)とMVP(Passive View)についてブログを書く

Retirement note for Model View Presenter Pattern

  • 2005: MVP(Passive View)がMichael Feathersらの論文で発表

MVP(Supervising Controller)とMVP(Passive View)の違い

  • というわけで、MVP(Supervising Controller)とMVP(Passive View)に大別できるんだけども、基本的にはMVP(Passive View)だけ考えればよさそう
  • 両者の違いは、View - Model間のオブザーバー同期を許すかどうか
  • MVP(Supervising Controller)は、フロー同期とオブザーバー同期の二通りがある
    • なおオブザーバー同期を使った場合の設計は完全に原初MVCと同じになる
  • MVP(Passive View)はCocoa MVCとほぼ一致する。が、Cocoa MVCは別にオブザーバー同期してはいけませんよ、とは言ってないので、MVPとも言えないのかな。フロー同期前提ではあるので、だいたいの場合Cocoa MVCっぽくなると思う

フロー同期とオブザーバー同期の違い

  • フロー同期は、オブジェクト生成時にお互いの参照を持ち合って、データを同期する仕組み。iOSで言えば、イニシャライザの引数に持たせたり、publicなプロパティとして持ったりすることで実現する
  • オブザーバー同期は、参照を持たず、共通の変数の監視によってデータを疎結合に同期する仕組み。iOSで言えば、NotificationCenterやCombineで実現する
  • どちらも一長一短で、フロー同期はコードとして処理が追いやすいが、互いの参照を持つので密結合になったり、参照型故のバグが発生したりする。オブザーバー同期はその逆

実装してみた

MVP(Passive View)の実装がこちらです。

https://github.com/0si43/iOS-architecture-training/tree/MVP

ノート右図のようなフローにすることを意識しました。

参考書との違い

参考書のサンプルがこちらなんですが、

https://github.com/peaks-cc/iOS_architecture_samplecode/tree/master/06/MVPSample

こちらはUIKitで、View = ViewControllerという発想で実装しています。
ただ今回はSwiftUIを使っているので、PresenterにUIViewControllerを継承させることにしました。
そもそもSwiftUIがプレゼンテーションロジックを持っているので、そのままやるとMVC編のように、Presenterのお仕事がなくなってしまうので、
MVPの主眼である、プレゼンテーションロジックを持たないViewとして再設計しました。

とはいえ

とはいえ、UIKitのようにViewの参照を持って、都度更新をかける、というのがSwiftUIだとできません。
というわけで、SwiftUIのViewにこんなEnumを持たせました。

    let type: StateType
    enum StateType {
        case display([User])
        case notFound
        case error(ModelError)
    }

PresenterはViewを生成するときにこのtypeを指定します。
画面更新の際は、新たなViewをPresenter側で生成して、表示する仕様にしました。

もしViewの起動パターンがある程度しぼれるなら、こういうEnum定義しておくとキレイだなと思いました。
何がいいって、プレビューで全パターンを並べて見られるのが気持ちいですね。

「Viewがテストしづらい」という前提が崩れているかも

Passive Viewというアーキテクチャーは、そもそも「Viewがテストしづらい」という前提から来ています。
しかし宣言的UIは以前と比べると、プレビューでパッと見ることができるので、前提が変わってるかもしれません。
もちろん自動テストを考えたときにやりづらいのは以前として事実です。

SwiftUIでPresenter書くと、辛さを感じるところもあリました。
SwiftUIで素朴に書くんなら、ViewとModelだけで事足りるよなあと思いました。

画面遷移が辛かった

SwiftUIでPresenter書くと辛かったところはいくつかあるんですが、最終的に一番大きかったのは画面遷移でした。
SwiftUIのNavigationLink→Presenterのメソッド→Modelのload処理→その結果を受け取ったPresenterが新View(SwiftUI)を返す、というのを書きたかったんですが、
クロージャーを使ったコールバックで書くと、Presenterが新Viewを返そうとしても、返す先がないんですよね。

結局ここは挫折しました。

まとめ

ということでMVPでした。
個人的にはPresenterというのは、曖昧だったControllerの責務を厳密化しているだけなのかなという感じがしました。
テスト容易性が上がる、というのは間違いなくいいことなので、MVCよりはいい気がします。

ただプレゼンテーションロジックを宣言的UIのフレームワークが吸収したところがあるので、その環境下であってもMVPにメリットが残ってるかはちょっと疑問でした。

Discussion