静的パターンの操作:SwiftMVVMチュートリアル

公開: 2022-03-11

今日は、リアルタイムのデータ駆動型アプリケーションに対するユーザーからの新しい技術的可能性と期待が、プログラム、特にモバイルアプリケーションの構造に新しい課題をどのように生み出すかを見ていきます。 この記事はiOSとSwiftに関するものですが、パターンと結論の多くはAndroidとWebアプリケーションに等しく適用できます。

過去数年間、最新のモバイルアプリの動作には重要な進化がありました。 プッシュ通知やWebSocketなどのより普及したインターネットアクセスとテクノロジーのおかげで、今日のモバイルアプリの多くでは、ユーザーは通常、ランタイムイベントの唯一のソースではなく、必ずしも最も重要なものではありません。

2つのSwiftデザインパターンがそれぞれ最新のチャットアプリケーションでどの程度うまく機能するかを詳しく見てみましょう。クラシックモデルビューコントローラー(MVC)パターンと単純化された不変モデルビュービューモデルパターン(MVVM、場合によっては「ViewModelパターン」 」)。 チャットアプリは、多くのデータソースがあり、データを受信するたびにさまざまな方法でUIを更新する必要があるため、良い例です。

私たちのチャットアプリケーション

このSwiftMVVMチュートリアルでガイドラインとして使用するアプリケーションには、WhatsAppなどのチャットアプリケーションでわかっている基本的な機能のほとんどが含まれています。 実装する機能を確認し、MVVMとMVCを比較してみましょう。 アプリケーション:

  • 以前に受信したチャットをディスクからロードします
  • GETリクエストを介して既存のチャットをサーバーと同期します
  • 新しいメッセージがユーザーに送信されたときにプッシュ通知を受信します
  • チャット画面に入ると、WebSocketに接続されます
  • チャットに新しいメッセージをPOSTできます
  • 現在参加していないチャットの新しいメッセージを受信すると、アプリ内通知が表示されます
  • 現在のチャットの新しいメッセージを受信すると、すぐに新しいメッセージが表示されます
  • 未読メッセージを読んだときに既読メッセージを送信します
  • 誰かが私たちのメッセージを読むと、既読メッセージを受け取ります
  • アプリケーションアイコンの未読メッセージカウンターバッジを更新します
  • 受信または変更されたすべてのメッセージをCoreDataに同期します

このデモアプリケーションでは、モデルの実装をもう少しシンプルに保つための実際のAPI、WebSocket、またはCoreDataの実装はありません。 代わりに、会話を開始すると返信を開始するチャットボットを追加しました。 ただし、他のすべてのルーティングと呼び出しは、ストレージと接続が実際にある場合と同じように実装されます。これには、戻る前の小さな非同期一時停止も含まれます。

次の3つの画面が作成されました。

[チャットリスト]、[チャットの作成]、および[メッセージ]画面。

クラシックMVC

まず、iOSアプリケーションを構築するための標準のMVCパターンがあります。 これは、Appleがすべてのドキュメントコードを構築する方法であり、APIとUI要素が機能することを期待する方法です。 これは、ほとんどの人がiOSコースを受講するときに教えられることです。

多くの場合、MVCは、数千行のコードの肥大化したUIViewControllerにつながると非難されます。 しかし、それがうまく適用され、各レイヤーが適切に分離されていれば、 ViewModel 、および他のController間の中間マネージャーのようにのみ機能する非常にスリムなViewControllerを使用できます。

アプリのMVC実装のフローチャートは次のとおりです(わかりやすくするためにCreateViewControllerを省略しています)。

わかりやすくするためにCreateViewControllerを省略したMVC実装フローチャート。

レイヤーを詳しく見ていきましょう。

モデル

モデルレイヤーは通常、MVCで最も問題の少ないレイヤーです。 この場合、 ChatWebSocketChatModel 、およびPushNotificationControllerを使用して、 ChatオブジェクトとMessageオブジェクト、外部データソース、およびアプリケーションの残りの部分の間を仲介することを選択しました。 ChatModelは、アプリケーション内の信頼できる情報源であり、このデモアプリケーションではメモリ内でのみ機能します。 実際のアプリケーションでは、おそらくCoreDataによってサポートされます。 最後に、 ChatEndpointはすべてのHTTP呼び出しを処理します。

意見

すべてのビューコードをUIViewControllerから慎重に分離したため、ビューは多くの責任を処理する必要があるため、非常に大きくなります。 私は次のことをしました:

  • (非常に推奨される)状態enumパターンを使用して、ビューが現在どのような状態にあるかを定義しました。
  • ボタンやその他のアクショントリガーインターフェイスアイテムに接続する機能を追加しました(連絡先名の入力中に[戻る]をタップするなど)。
  • 制約を設定し、毎回デリゲートにコールバックします。

UITableViewを組み合わせてスローすると、ビューはUIViewControllerよりもはるかに大きくなり、 UIViewControllerで300行以上の厄介なコードと多くの混合タスクがChatViewします。

コントローラ

すべてのモデル処理ロジックがChatModelに移動したため。 すべてのビューコード(ここでは最適ではない分離されたプロジェクトに潜んでいる可能性があります)がビューに存在するため、 UIViewControllerはかなりスリムです。 ビューコントローラは、モデルデータがどのように表示されるか、データがどのようにフェッチされるか、またはどのように表示されるかを完全に認識しません。座標だけです。 サンプルプロジェクトでは、 UIViewControllerのいずれも150行を超えるコードはありません。

ただし、ViewControllerは引き続き次のことを行います。

  • ビューおよびその他のビューコントローラーの代理人になる
  • 必要に応じて、View Controllerをインスタンス化してプッシュ(またはポップ)します
  • ChatModelとの間の通話の送受信
  • ビューコントローラサイクルの段階に応じたWebSocketの開始と停止
  • メッセージが空の場合はメッセージを送信しないなどの論理的な決定
  • ビューを更新する

これはまだたくさんありますが、ほとんどの場合、調整、コールバックブロックの処理、および転送です。

利点

  • このパターンは誰もが理解し、Appleによって推進されています
  • すべてのドキュメントで動作します
  • 追加のフレームワークは必要ありません

欠点

  • ビューコントローラには多くのタスクがあります。 それらの多くは基本的に、ビューとモデルレイヤーの間でデータをやり取りしています。
  • 複数のイベントソースを処理するにはあまり適していません
  • クラスは他のクラスについてよく知っている傾向があります

問題の定義

これは、AdobePhotoshopやMicrosoftWordのようなアプリケーションが機能することを想像するように、アプリケーションがユーザーのアクションに従い、ユーザーのアクションに応答する限り、非常にうまく機能します。 ユーザーがアクションを実行し、UIが更新され、繰り返します。

しかし、最近のアプリケーションは、多くの場合、複数の方法で接続されています。 たとえば、REST APIを介して対話し、プッシュ通知を受信し、場合によってはWebSocketにも接続します。

これにより、突然、View Controllerはより多くの情報ソースを処理する必要があり、WebSocketを介したメッセージの受信など、ユーザーがトリガーせずに外部メッセージを受信するたびに、情報ソースは右側に戻る必要があります。ビューコントローラー。 これには、基本的に同じタスクを実行するためにすべてのパーツを接着するためだけに多くのコードが必要です。

外部データソース

プッシュメッセージを受け取ったときに何が起こるかを見てみましょう。

 class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure("Chat for received message should always exist") } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } }

プッシュ通知を受け取った後に自分自身を更新する必要があるViewControllerがあるかどうかを判断するには、ViewControllerのスタックを手動で調べなければなりません。 この場合、 UpdatedChatDelegateを実装する画面も更新する必要があります。この場合はChatsViewControllerのみです。 また、これは、通知が意図されたChatをすでに確認しているため、通知を抑制する必要があるかどうかを知るために行います。 その場合、代わりに最終的にメッセージをViewControllerに配信します。 PushNotificationControllerがその作業を実行できるようにするには、アプリケーションについてあまりにも多くのことを知る必要があることは明らかです。

ChatWebSocketChatViewControllerと1対1の関係を持つのではなく、アプリケーションの他の部分にもメッセージを配信する場合、そこで同じ問題に直面します。

別の外部ソースを追加するたびに、非常に侵襲的なコードを作成する必要があることは明らかです。 このコードは、アプリケーション構造に大きく依存し、データを階層に戻して機能させるため、非常に脆弱です。

代表者

MVCパターンは、他のView Controllerを追加すると、ミックスにさらに複雑さを追加します。 これは、ビューコントローラがデリゲート、初期化子、およびストーリーボードの場合はデータと参照を渡すときにprepareForSegueを介して相互に認識し合う傾向があるためです。 すべてのViewControllerは、モデルまたは仲介コントローラーへの独自の接続を処理し、更新の送信と受信の両方を行います。

また、ビューはデリゲートを介してビューコントローラと通信します。 これは機能しますが、データを渡すために実行する必要のある手順が非常に多いことを意味します。コールバックの周りで多くのリファクタリングを行い、デリゲートが実際に設定されているかどうかを確認しています。

ChatViewControllerupdated(chat: Chat)を呼び出さなくなったため、 ChatsListViewControllerの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

ビューコントローラからcustomViewにすべてのビュー関連コードを削除し、モデル関連コードをすべて専用コントローラに移動することで、ビューコントローラはかなりスリムで分離されています。 ただし、まだ1つの問題が残っています。ビューが表示したいものと、モデルに存在するデータとの間にギャップがあります。 良い例はChatListViewです。 表示したいのは、誰と話しているのか、最後のメッセージは何か、最後のメッセージの日付、 Chatに残っている未読メッセージの数を示すセルのリストです。

チャット画面の未読メッセージカウンター。

ただし、何を見たいのかわからないモデルを渡します。 代わりに、メッセージを含む連絡先との単なるChatです。

 class Chat { let contact: String var messages: [Message] init(with contact: String, messages: [Message] = []) { self.contact = contact self.messages = messages } }

これで、最後のメッセージとメッセージ数を取得するコードをすばやく追加できますが、日付を文字列にフォーマットすることは、ビューレイヤーにしっかりと属するタスクです。

 var unreadMessages: Int { return messages.filter { switch ($0.sender, $0.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }

最後に、日付を表示するときに、 ChatItemTableViewCellで日付をフォーマットします。

 func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? "" lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? "" show(unreadMessageCount: chat.unreadMessages) }

かなり単純な例でも、ビューに必要なものとモデルが提供するものの間に緊張関係があることは明らかです。

静的イベント駆動型MVVM、別名静的イベント駆動型の「ViewModelパターン」のテイク

静的MVVMはビューモデルで動作しますが、MVCを使用してビューコントローラーを介して行っていたように、ビューモデルを介して双方向トラフィックを作成する代わりに、イベントに応じてUIを変更する必要があるたびにUIを更新する不変のビューモデルを作成します。

イベントenumに必要な関連データを提供できる限り、コードのほぼすべての部分でイベントをトリガーできます。 たとえば、 received(new: Message)イベントの受信は、プッシュ通知、WebSocket、または通常のネットワーク呼び出しによってトリガーできます。

それを図で見てみましょう:

MVVMの実装フローチャート。

一見すると、まったく同じことを達成するために関係するクラスがはるかに多いため、従来のMVCの例よりもかなり複雑に見えます。 しかし、詳しく調べてみると、どの関係も双方向ではありません。

さらに重要なのは、UIのすべての更新がイベントによってトリガーされるため、発生するすべてのことについてアプリを通過するルートは1つだけであるということです。 予想できるイベントはすぐにわかります。 また、必要に応じて新しい動作を追加したり、既存のイベントに応答するときに新しい動作を追加したりする必要がある場所も明確です。

上で示したように、リファクタリングした後、私は多くの新しいクラスに行き着きました。 静的MVVMバージョンの私の実装はGitHubで見つけることができます。 ただし、変更をclocツールと比較すると、実際にはそれほど多くの余分なコードがないことが明らかになります。

パターンファイル空欄コメントコード
MVC 30 386 217 1807年
MVVM 51 442 359 1981年

コード行の増加はわずか9%です。 さらに重要なことに、これらのファイルの平均サイズは、60行のコードからわずか39行に減少しました。

コード行の円グラフ。ビューコントローラー:MVC287とMVVM154または47%少ない;ビュー:MVC523とMVVM392または26%少ない。

また重要なことに、最大のドロップは、通常MVCで最大のファイル(ビューとビューコントローラー)にあります。 ビューは元のサイズのわずか74%であり、ビューコントローラーは元のサイズの53%にすぎません。

余分なコードの多くは、MVCの従来の@IBActionまたはデリゲートパターンを必要とせずに、ビジュアルツリー内のボタンやその他のオブジェクトにブロックをアタッチするのに役立つライブラリコードであることにも注意してください。

このデザインのさまざまなレイヤーを1つずつ見ていきましょう。

イベント

イベントは常にenumであり、通常は値が関連付けられています。 多くの場合、モデル内のエンティティの1つと重複しますが、必ずしもそうではありません。 この場合、アプリケーションは、 ChatEventMessageEventの2つのメインイベントenum型に分割されます。 ChatEventは、チャットオブジェクト自体のすべての更新用です。

 enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

もう1つは、メッセージ関連のすべてのイベントを処理します。

 enum MessageEvent { case sending(message: Message, contact: String, previousMessages: [Message]) case sent(message: Message, contact: String) case failedSending(message: Message, contact: String, reason: String) case received(message: Message, contact: String) case userReads(messagesSentBy: String) case userRead(othersMessages: [Message], sentBy: String) case otherRead(yourMessage: Message, reader: String) }

*Event enumを適切なサイズに制限することが重要です。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注: enumの概念は、Swiftでは非常に強力です。 列挙型は、オプションの値を使用した場合のあいまいさを取り除くことができるため、値が関連付けられたenumを頻繁に使用する傾向があります。

Swift MVVMチュートリアル:イベントルーター

イベントルーターは、アプリケーションで発生するすべてのイベントのエントリポイントです。 関連する値を提供できるクラスは、イベントを作成してイベントルーターに送信できます。 したがって、それらはあらゆる種類のソースによってトリガーできます。例:

  • 特定のViewControllerにセグエするユーザー
  • ユーザーが特定のボタンをタップする
  • アプリケーションの開始
  • 次のような外部イベント:
    • 障害または新しいデータで返されるネットワーク要求
    • プッシュ通知
    • WebSocketメッセージ

イベントルーターは、イベントのソースについてできるだけ知らないようにする必要があり、できれば何も知らないようにする必要があります。 このサンプルアプリケーションのイベントには、それらがどこから来たのかを示すインジケーターがないため、あらゆる種類のメッセージソースに簡単に混在させることができます。 たとえば、WebSocketは、新しいプッシュ通知と同じイベントreceived(message: Message, contact: String)トリガーします。

イベントは(すでに推測しているように)これらのイベントをさらに処理する必要があるクラスにルーティングされます。 通常、呼び出されるクラスは、モデルレイヤー(データを追加、変更、または削除する必要がある場合)とイベントハンドラーのみです。 両方についてもう少し詳しく説明しますが、イベントルーターの主な機能は、すべてのイベントに1つの簡単なアクセスポイントを提供し、作業を他のクラスに転送することです。 例として、 ChatEventRouterを次に示します。

 class ChatEventRouter { static func route(event: ChatEvent) { switch event { case .loaded(let chats): ChatEventHandler.loaded(chats: chats) case .creatingChat(let contact): let chat = ChatModel.create(chatWith: contact) ChatEndpoint.create(chat: chat) ChatEventHandler.creatingChat() case .created(let chat): ChatEventHandler.created(chat: chat) case .createChatFailed(let reason): ChatEventHandler.failedCreatingChat(reason: reason) } } }

ここではほとんど何も起こっていません。私たちが行っているのは、モデルを更新し、イベントをChatEventHandlerに転送して、UIが更新されるようにすることだけです。

Swift MVVMチュートリアル:モデルコントローラー

これは、MVCで使用しているクラスとまったく同じです。これは、すでにかなりうまく機能していたためです。 これはアプリケーションの状態を表し、通常はCoreDataまたはローカルストレージライブラリによってサポートされます。

モデルレイヤー(MVCで正しく実装されている場合)は、さまざまなパターンに合わせるためにリファクタリングを必要とすることはめったにありません。 最大の変更点は、モデルの変更が少数のクラスから行われることであり、変更が行われる場所が少し明確になります。

このパターンの別の方法では、モデルへの変更を観察し、それらが処理されることを確認できます。 この場合、 *EventRouterクラスと*Endpointクラスのみにモデルを変更させることを選択したので、モデルが更新される場所と時期について明確な責任があります。 対照的に、変更を監視している場合は、 ChatEventHandlerを介してエラーなどのモデルを変更しないイベントを伝播する追加のコードを作成する必要があります。これにより、イベントがアプリケーションをどのように流れるかがわかりにくくなります。

Swift MVVMチュートリアル:イベントハンドラー

イベントハンドラーは、ビューまたはビューコントローラーがリスナーとして登録(および登録解除)して、 ChatEventHandler ChatEventRouter関数を呼び出すたびに構築される更新されたビューモデルを受信できる場所です。

これは、以前にMVCで使用したすべてのビューステートを大まかに反映していることがわかります。 サウンドやTapticエンジンのトリガーなど、他のタイプのUI更新が必要な場合は、ここからも実行できます。

 protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } }

このクラスは、特定のイベントが発生したときに、適切なリスナーが適切なビューモデルを取得できることを確認するだけです。 新しいリスナーは、初期状態を設定する必要がある場合、追加されたときにすぐにビューモデルを取得できます。 保持サイクルを防ぐために、リストにweak参照を追加するようにしてください。

Swift MVVMチュートリアル:モデルの表示

これは、多くのMVVMパターンが実行することと静的バリアントが実行することの最大の違いの1つです。 この場合、ビューモデルは、モデルとビューの間の永続的な双方向の中間として設定されるのではなく、不変です。 なぜそうするのでしょうか? それを少し説明するために一時停止しましょう。

考えられるすべての場合に適切に機能するアプリケーションを作成する上で最も重要な側面の1つは、アプリケーションの状態が正しいことを確認することです。 UIがモデルと一致しないか、古いデータがある場合、私たちが行うすべてのことにより、誤ったデータが保存されたり、アプリケーションがクラッシュしたり、予期しない動作をしたりする可能性があります。

このパターンを適用する目的の1つは、絶対に必要でない限り、アプリケーションに状態がないことです。 正確には、状態とは何ですか? 状態は基本的に、特定のタイプのデータの表現を格納するすべての場所です。 特別なタイプの状態の1つは、UIが現在ある状態です。もちろん、UI駆動型アプリケーションではこれを防ぐことはできません。 他のタイプの状態はすべてデータに関連しています。 チャットリスト画面にUITableViewをバックアップするChatの配列のコピーがある場合、それは重複状態の例です。 従来の双方向バウンドビューモデルは、ユーザーのChatの複製のもう1つの例です。

モデルが変更されるたびに更新される不変のビューモデルを渡すことで、このタイプの重複状態を排除します。これは、UIに適用された後は、使用されなくなるためです。 その場合、回避できない状態はUIとモデルの2種類だけであり、それらは互いに完全に同期しています。

したがって、ここでのビューモデルは、一部のMVVMアプリケーションとはかなり異なります。 これは、ビューがモデルの状態を反映するために必要なすべてのフラグ、値、ブロック、およびその他の値の不変のデータストアとしてのみ機能しますが、ビューによって更新することはできません。

したがって、単純な不変のstructにすることができます。 このstructをできるだけ単純に保つために、ビューモデルビルダーを使用してインスタンス化します。 ビューモデルの興味深い点の1つは、以前にビューで見つかった状態enum型メカニズムを置き換える、 shouldShowBusyshouldShowErrorなどの動作フラグを取得することです。 以前に分析したChatItemTableViewCellのデータは次のとおりです。

 struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }

ビューモデルビルダーは、ビューに必要な正確な値とアクションを既に処理しているため、すべてのデータは事前​​にフォーマットされています。 また、アイテムがタップされるとトリガーされるブロックも新しくなっています。 ビューモデルビルダーによってどのように作成されるかを見てみましょう。

モデルビルダーを表示

ビューモデルビルダーは、ビューモデルのインスタンスを構築し、 ChatMessageなどの入力を特定のビューに完全に合わせたビューモデルに変換できます。 ビューモデルビルダーで発生する最も重要なことの1つは、ビューモデルのブロック内で実際に何が発生するかを判断することです。 ビューモデルビルダーによってアタッチされるブロックは非常に短く、アーキテクチャの他の部分の関数をできるだけ早く呼び出す必要があります。 このようなブロックには、ビジネスロジックを含めるべきではありません。

 class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? "" let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? "" let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }

これで、すべての事前フォーマットが同じ場所で行われ、動作もここで決定されます。 これはこの階層で非常に重要なクラスであり、デモアプリケーションのさまざまなビルダーがどのように実装され、より複雑なシナリオを処理するかを確認することは興味深い場合があります。

Swift MVVMチュートリアル:View Controller

このアーキテクチャのViewControllerはほとんど何もしません。 ビューに関連するすべてのものをセットアップして破棄します。 適切なタイミングでリスナーを追加および削除するために必要なすべてのライフサイクルコールバックを取得するため、これを行うのが最適です。

タイトルやナビゲーションバーのボタンなど、ルートビューでカバーされていないUI要素を更新する必要がある場合があります。 そのため、特定のビューコントローラーのビュー全体をカバーするビューモデルがある場合は、通常、ビューコントローラーをイベントルーターのリスナーとして登録します。 その後、ビューモデルをビューに転送します。 ただし、特定の会社に関するページの上部にある株式相場表示など、更新レートが異なる画面の一部がある場合は、 UIViewをリスナーとして直接登録することもできます。

ChatsViewControllerのコードが非常に短いため、1ページ未満で済みます。 残っているのは、ベースビューのオーバーライド、ナビゲーションバーからの追加ボタンの追加と削除、タイトルの設定、リスナーとしての自身の追加、およびChatListListeningプロトコルの実装です。

 class ChatsViewController: UIViewController { private lazy var customView: ChatsView = { let customView = ChatsView() return customView }() private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) override func loadView() { view = customView } override func viewDidLoad() { super.viewDidLoad() ChatEventHandler.add(listener: self) ChatEventRouter.route(event: .started) title = "Chats" } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationItem.rightBarButtonItem = addButton } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationItem.rightBarButtonItem = nil } } extension ChatsViewController: ChatListListening { func updated(list: ChatListViewModel) { addButton.action(block: { _ in list.addChat() }) customView.display(viewModel: list) } }

ChatsViewControllerが最小限に抑えられているため、他の場所で実行できることは何も残っていません。

Swift MVVMチュートリアル:表示

不変のMVVMアーキテクチャのビューは、タスクのリストがまだあるため、依然としてかなり重い可能性がありますが、MVCアーキテクチャと比較して、次の責任を取り除くことができました。

  • 新しい状態に応じて何を変更する必要があるかを判断する
  • アクションのデリゲートと関数の実装
  • ジェスチャーやトリガーされたアニメーションなどのビューツービュートリガーを処理する
  • 表示できるようにデータを変換する( DateStringなど)

特に最後のポイントにはかなり大きなアドバンテージがあります。 MVCでは、ビューまたはビューコントローラーが表示用のデータの変換を担当する場合、このスレッドで発生する必要のあるUIへの実際の変更を、メインスレッドで行う必要があるものから分離するのは非常に難しいため、常にメインスレッドでこれを行います。その上で実行する必要はありません。 また、UI変更以外のコードをメインスレッドで実行すると、アプリケーションの応答性が低下する可能性があります。

代わりに、このMVVMパターンでは、タップによってトリガーされたブロックから、ビューモデルが構築されてリスナーに渡されるまでのすべてが、別のスレッドで実行され、メインスレッドにのみディップできます。 UI更新を行うために終了します。 アプリケーションがメインスレッドに費やす時間が少ない場合、アプリケーションはよりスムーズに実行されます。

ビューモデルが新しい状態をビューに適用すると、状態の別のレイヤーとして残り続けるのではなく、蒸発することができます。 イベントをトリガーする可能性のあるものはすべてビュー内のアイテムに関連付けられており、ビューモデルに連絡することはありません。

覚えておくべき重要なことが1つあります。それは、ビューコントローラーを介してビューモデルをビューにマップする必要がないことです。 前述のように、ビューの一部は、特に更新レートが異なる場合に、他のビューモデルで管理できます。 共同編集者のためにチャットペインを開いたまま、さまざまな人がGoogleスプレッドシートを編集していると考えてください。チャットメッセージが届くたびに、ドキュメントを更新することはあまり役に立ちません。

よく知られている例は、検索タイプの実装です。この実装では、テキストを入力すると、検索ボックスがより正確な結果で更新されます。 これは、 CreateAutocompleteViewクラスにオートコンプリートを実装する方法です。画面全体がCreateViewModelによって提供されますが、テキストボックスは代わりにAutocompleteContactViewModelをリッスンします。

もう1つの例は、フォームバリデーターを使用することです。これは、「ローカルループ」(フィールドへのエラー状態のアタッチまたは削除、およびフォームの有効性の宣言)として構築するか、イベントをトリガーすることで実行できます。

静的不変ビューモデルは、より優れた分離を提供します

静的なMVVM実装を使用することで、ビューモデルがモデルとビューの間を橋渡しするようになったため、最終的にすべてのレイヤーを完全に分離することができました。 また、ユーザーの操作によって引き起こされたのではないイベントの管理を容易にし、アプリケーションのさまざまな部分間の多くの依存関係を削除しました。 ビューコントローラが行う唯一のことは、受信したいイベントのリスナーとして、イベントハンドラに自分自身を登録(および登録解除)することです。

利点:

  • ビューおよびビューコントローラの実装ははるかに軽量になる傾向があります
  • クラスはより専門的で分離されています
  • イベントはどこからでも簡単にトリガーできます
  • イベントは、システム内の予測可能なパスをたどります
  • 状態は1か所からのみ更新されます
  • メインスレッドからの作業が簡単になるため、アプリのパフォーマンスが向上する可能性があります
  • Views receive tailor-made view models and are perfectly separated from the models

欠点:

  • A full view model is created and sent every time the UI needs to update, often overwriting the same button text with the same button text, and replacing blocks with blocks that do exactly the same
  • Requires some helper extensions to make button taps and other UI events work well with the blocks in the view model
  • Event enum s can easily grow pretty large in complex scenarios and might be hard to split up

The great thing is that this is a pure Swift pattern: It does not require a third-party Swift MVVM framework, nor does it exclude the use of classic MVC, so you can easily add new features or refactor problematic parts of your application today without being forced to rewrite your whole application.

There are other approaches to combat large view controllers that provide better separation as well. I couldn't include them all in full detail to compare them, but let's take a brief look at some of the alternatives:

  • Some form of the MVVM pattern
  • Some form of Reactive (using RxSwift, sometimes combined with MVVM)
  • The model-view-presenter pattern (MVP)
  • The view-interactor-presenter-entity-router pattern (VIPER)

Traditional MVVM replaces most of the view controller code with a view model that is just a regular class and can be tested more easily in isolation. Since it needs to be a bi-directional bridge between the view and the model it often implements some form of Observables. That's why you often see it used together with a framework like RxSwift.

MVP and VIPER deal with extra abstraction layers between the model and the view in a more traditional way, while Reactive really remodels the way data and events flow through your application.

The Reactive style of programming is gaining a lot of popularity lately and actually is pretty close to the static MVVM approach with events, as explained in this article. The major difference is that it usually requires a framework, and a lot of your code is specifically geared towards that framework.

MVP is a pattern where both the view controller and the view are considered to be the view layer. The presenter transforms the model and passes it to the view layer, while I transform the data into a view model first. Since the view can be abstracted to a protocol, it's much easier to test.

VIPER takes the presenter from MVP, adds a separate “interactor” for business logic, calls the model layer “entity,” and has a router for navigation purposes (and to complete the acronym). It can be considered a more detailed and decoupled form of MVP.


So there you have it: static event-driven MVVM explained. I look forward to hearing from you in the comments below!

Related: Swift Tutorial: An Introduction to the MVVM Design Pattern