.sender,

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

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

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

MVVM実装フローチャート。

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

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

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

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

Message

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

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

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

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

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

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

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

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

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

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

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

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

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

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

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

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

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

.state) { case (.user, _), (.other, .read): return false case (.other, .sending), (.other, .sent): return true } }.count } var lastMessage: Date? { return messages.last?.sendDate }
でフォーマットします表示するとき:

ChatItemTableViewCell

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

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

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

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

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

MVVM実装フローチャート。

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

Tableauなどのデータ視覚化ツール

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

このクラスは、特定のイベントが発生したときに、適切なリスナーが適切なビューモデルを取得できることを確認するだけです。新しいリスナーは、初期状態を設定する必要がある場合、追加されたときにすぐにビューモデルを取得できます。必ず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 {

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

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

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

MVVM実装フローチャート。

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

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

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

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

Message

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

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

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

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

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

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

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

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

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

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

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

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

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

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

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

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

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

!== 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 {

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

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

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

MVVM実装フローチャート。

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

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

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

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

Message

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

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

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

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

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

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

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

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

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

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

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

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

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

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

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

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

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

!== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach {

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

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

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

MVVM実装フローチャート。

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

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

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

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

Message

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

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

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

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

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

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

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

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

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

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

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

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

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

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

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

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

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach {

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

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

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

MVVM実装フローチャート。

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

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

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

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

Message

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

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

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

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

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

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

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

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

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

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

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

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

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

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

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

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

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach {

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

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

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

MVVM実装フローチャート。

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

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

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

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

Message

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

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

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

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

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

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

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

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

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

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

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

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

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

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

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

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

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach {

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

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

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

MVVM実装フローチャート。

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

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

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

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

Message

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

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

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

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

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

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

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

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

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

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

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

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

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

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

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

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

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach {

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

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

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

MVVM実装フローチャート。

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

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

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

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

Message

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

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

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

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

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

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

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

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

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

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

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

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

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

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

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

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

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

?.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 {

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

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

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

MVVM実装フローチャート。

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

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

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

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

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

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

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

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

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

イベント

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

ChatEvent

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

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

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

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

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

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

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

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) } } } に転送することだけです。そのため、UIが更新されます。

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

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

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

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

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

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

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

ChatEventHandler

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

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

Message

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

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

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

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

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

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

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

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

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

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

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

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

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

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

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

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

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

?.updated(list: chatListViewModel) } } }
を追加してください保持サイクルを防ぐためのリストへの参照。

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

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

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

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

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

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

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

ChatItemTableViewCell

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

モデルビルダーを表示

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

ビジュアルコミュニケーションデザインとグラフィックデザイン
Message

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

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

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

タイトルやナビゲーションバーのボタンなど、ルートビューでカバーされていないUI要素を更新する必要がある場合があります。そのため、特定のビューコントローラーのビュー全体をカバーするビューモデルがある場合は、通常、ビューコントローラーをイベントルーターのリスナーとして登録します。その後、ビューモデルをビューに転送します。ただし、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:

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

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

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

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



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

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

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

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

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

クラシックMVC

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

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

これがのフローチャートです アプリのMVC実装 (わかりやすくするためにControllerは省略しています):

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

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

モデル

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

見る

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

enumを投げたらミックスでは、ビューがUITableView sよりもはるかに大きくなり、気になる300行以上のコードと、UIViewControllerでの多くの混合タスクが発生します。

コントローラ

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

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

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

利点

欠点

問題の定義

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

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

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

外部データソース

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

ChatModel

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

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

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

代表団

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

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

prepareForSegueの古いデータのように、別のコードを変更することで、あるViewControllerを壊すことができます。 ChatsListViewControllerだから呼び出していませんChatViewControllerもう。特により複雑なシナリオでは、すべての同期を維持するのは面倒です。

ビューとモデルの分離

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

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

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

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

イベントは、イベント 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) } に必要な関連データを提供できる限り、コードのほぼすべての部分でトリガーできます。たとえば、enumを受信しますイベントは、プッシュ通知、WebSocket、または通常のネットワーク呼び出しによってトリガーできます。

それを図で見てみましょう:

MVVM実装フローチャート。

一見すると、まったく同じことを達成するために関係するクラスがはるかに多いため、従来のMVCの例よりもかなり複雑に見えます。しかし、詳しく調べてみると、どの関係も双方向ではありません。

さらに重要なのは、UIのすべての更新がイベントによってトリガーされるため、発生するすべてのことについてアプリを通るルートが1つしかないことです。予想できるイベントはすぐにわかります。また、必要に応じて新しい動作を追加する場所や、既存のイベントに応答するときに新しい動作を追加する場所も明確です。

上に示したように、リファクタリングした後、私は多くの新しいクラスに行き着きました。静的MVVMバージョンの私の実装を見つけることができます GitHubで 。ただし、変更をreceived(new: Message)と比較するとツールを使用すると、実際にはそれほど多くの余分なコードがないことが明らかになります。

パターン ファイル ブランク コメント コード
MVC 30 386 217 1807年
MVVM 51 442 359 1981年

コード行の増加はわずか9%です。さらに重要なことに、これらのファイルの平均サイズは60行のコードからわずか39行に減少しました。

コード行の円グラフ。ビューコントローラー:MVC287とMVVM154または47%少ない;ビュー:MVC523対MVVM392または26%少ない。

また重要なことに、最大のドロップは、通常MVCで最大のファイル(ビューとビューコントローラー)にあります。ビューは元のサイズのわずか74%であり、ビューコントローラーは元のサイズの53%にすぎません。

余分なコードの多くは、MVCの従来のclocを必要とせずに、ビジュアルツリー内のボタンやその他のオブジェクトにブロックをアタッチするのに役立つライブラリコードであることにも注意してください。またはパターンを委任します。

このデザインのさまざまなレイヤーを1つずつ見ていきましょう。

イベント

イベントは常に@IBActionであり、通常は値が関連付けられています。多くの場合、モデル内のエンティティの1つと重複しますが、必ずしもそうとは限りません。この場合、アプリケーションは2つのメインイベントenum sに分割されます:enumおよびChatEventMessageEventチャットオブジェクト自体のすべての更新用です。

ChatEvent

もう1つは、メッセージ関連のすべてのイベントを処理します。

enum ChatEvent { case started case loaded(chats: [Chat]) case creating(chat: Chat) case created(chat: Chat) case createChatFailed(reason: String) }

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 sを適切なサイズにします。 10以上のケースが必要な場合、それは通常、複数の主題をカバーしようとしている兆候です。

注:enumコンセプトはSwiftで非常に強力です。私はenum sを関連する値と一緒に使用する傾向があります。これは、オプションの値を使用した場合のあいまいさを大​​幅に取り除くことができるためです。

Swift MVVMチュートリアル:イベントルーター

イベントルーターは、アプリケーションで発生するすべてのイベントのエントリポイントです。関連する値を提供できるクラスは、イベントを作成してイベントルーターに送信できます。したがって、それらはあらゆる種類のソースによってトリガーできます。

イベントルーターは、イベントのソースについてできるだけ知らないようにする必要があり、できれば何も知らないようにする必要があります。このサンプルアプリケーションのイベントには、それらがどこから来たのかを示すインジケーターがないため、あらゆる種類のメッセージソースに簡単に混在させることができます。たとえば、WebSocketは、新しいプッシュ通知と同じイベント(enum)をトリガーします。

イベントは(すでに推測しているように)これらのイベントをさらに処理する必要があるクラスにルーティングされます。通常、呼び出されるクラスは、モデルレイヤー(データを追加、変更、または削除する必要がある場合)とイベントハンドラーのみです。両方についてもう少し詳しく説明しますが、イベントルーターの主な機能は、すべてのイベントに1つの簡単なアクセスポイントを提供し、作業を他のクラスに転送することです。これがreceived(message: Message, contact: String)です例として:

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) } } } に転送することだけです。そのため、UIが更新されます。

Swift MVVMチュートリアル:モデルコントローラー

これは、すでにかなりうまく機能していたため、MVCで使用するクラスとまったく同じです。これはアプリケーションの状態を表し、通常はCoreDataまたはローカルストレージライブラリによってサポートされます。

モデルレイヤーは、MVCに正しく実装されている場合、さまざまなパターンに合わせるためにリファクタリングを必要とすることはめったにありません。最大の変更点は、モデルの変更がより少ないクラスから行われることであり、変更が行われる場所が少し明確になります。

このパターンの別の方法では、モデルへの変更を観察し、それらが処理されることを確認できます。この場合、私は単にChatEventHandlerのみを許可することを選択しましたおよび*EventRouterクラスはモデルを変更するため、モデルがいつどこで更新されるかについて明確な責任があります。対照的に、変更を観察している場合は、エラーなどのモデルを変更しないイベントを*Endpointを介して伝播する追加のコードを作成する必要があります。これにより、イベントがアプリケーションをどのように流れるかがわかりにくくなります。

Swift MVVMチュートリアル:イベントハンドラー

イベントハンドラーは、ビューまたはビューコントローラーがリスナーとして登録(および登録解除)して、ChatEventHandlerが作成される更新されたビューモデルを受信できる場所です。 ChatEventRouterで関数を呼び出します。

これは、以前にMVCで使用したすべてのビューステートを大まかに反映していることがわかります。サウンドやTapticエンジンのトリガーなど、他のタイプのUI更新が必要な場合は、ここからも実行できます。

ChatEventHandler

このクラスは、特定のイベントが発生したときに、適切なリスナーが適切なビューモデルを取得できることを確認するだけです。新しいリスナーは、初期状態を設定する必要がある場合、追加されたときにすぐにビューモデルを取得できます。必ず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) } } } を追加してください保持サイクルを防ぐためのリストへの参照。

Swift MVVMチュートリアル:モデルの表示

これは、多くのMVVMパターンが実行することと静的バリアントが実行することの最大の違いの1つです。この場合、ビューモデルは、モデルとビューの間の永続的な双方向の中間として設定されるのではなく、不変です。なぜそうするのでしょうか?少し一時停止して説明しましょう。

考えられるすべての場合に適切に機能するアプリケーションを作成する上で最も重要な側面の1つは、アプリケーションの状態が正しいことを確認することです。 UIがモデルと一致しないか、古いデータがある場合、私たちが行うすべてのことにより、誤ったデータが保存されたり、アプリケーションがクラッシュしたり、予期しない方法で動作したりする可能性があります。

このパターンを適用する目的の1つは、絶対に必要でない限り、アプリケーションに状態がないことです。正確には、状態とは何ですか?状態は基本的に、特定のタイプのデータの表現を格納するすべての場所です。特別なタイプの状態の1つは、UIが現在ある状態です。もちろん、UI駆動型アプリケーションではこれを防ぐことはできません。他のタイプの状態はすべてデータに関連しています。 weakをバックアップするChatの配列のコピーがある場合チャットリスト画面では、これは重複状態の例です。従来の双方向バウンドビューモデルは、ユーザーのUITableViewの複製のもう1つの例です。

モデルが変更されるたびに更新される不変のビューモデルを渡すことで、このタイプの重複状態を排除します。これは、UIに適用された後は、使用されなくなるためです。そうすると、回避できない状態はUIとモデルの2種類だけになり、それらは完全に同期しています。

したがって、ここでのビューモデルは、一部のMVVMアプリケーションとはかなり異なります。これは、ビューがモデルの状態を反映するために必要なすべてのフラグ、値、ブロック、およびその他の値の不変のデータストアとしてのみ機能しますが、ビューによって更新することはできません。

したがって、単純な不変Chatにすることができます。これを維持するにはstructできるだけ単純に、ビューモデルビルダーを使用してインスタンス化します。ビューモデルの興味深い点の1つは、structのような動作フラグを取得することです。およびshouldShowBusy状態を置き換えるshouldShowError以前にビューで見つかったメカニズム。これがenumのデータです以前に分析したことがあります。

ChatItemTableViewCell

ビューモデルビルダーは、ビューに必要な正確な値とアクションを既に処理しているため、すべてのデータは事前​​にフォーマットされています。また、アイテムがタップされるとトリガーされるブロックも新しくなっています。ビューモデルビルダーによってどのように作成されるかを見てみましょう。

モデルビルダーを表示

ビューモデルビルダーは、ビューモデルのインスタンスを構築し、struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void } sやChat sなどの入力を特定のビューに完全に合わせたビューモデルに変換できます。ビューモデルビルダーで発生する最も重要なことの1つは、ビューモデルのブロック内で実際に何が発生するかを判断することです。ビューモデルビルダーによってアタッチされるブロックは非常に短く、アーキテクチャの他の部分の関数をできるだけ早く呼び出す必要があります。このようなブロックには、ビジネスロジックを含めるべきではありません。

Message

これで、すべての事前フォーマットが同じ場所で行われ、動作もここで決定されます。これはこの階層で非常に重要なクラスであり、デモアプリケーションのさまざまなビルダーがどのように実装されているかを確認し、より複雑なシナリオを処理することは興味深い場合があります。

Swift MVVMチュートリアル:View Controller

このアーキテクチャのViewControllerはほとんど機能しません。それは、そのビューに関連するすべてのものをセットアップして破棄します。適切なタイミングでリスナーを追加および削除するために必要なすべてのライフサイクルコールバックを取得するため、これを行うのが最適です。

タイトルやナビゲーションバーのボタンなど、ルートビューでカバーされていないUI要素を更新する必要がある場合があります。そのため、特定のビューコントローラーのビュー全体をカバーするビューモデルがある場合は、通常、ビューコントローラーをイベントルーターのリスナーとして登録します。その後、ビューモデルをビューに転送します。ただし、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) } } を登録することもできます。更新レートが異なる画面の一部がある場合は、リスナーとして直接。特定の会社に関するページの上部にある株式相場表示。

UIViewのコード非常に短いので、1ページもかかりません。残っているのは、ベースビューのオーバーライド、ナビゲーションバーからの追加ボタンの追加と削除、タイトルの設定、リスナーとしての自身の追加、およびChatsViewControllerの実装です。プロトコル:

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

Swift MVVMチュートリアル:表示

不変のMVVMアーキテクチャのビューは、タスクのリストがまだあるため、依然としてかなり重い可能性がありますが、MVCアーキテクチャと比較して、次の責任を取り除くことができました。

特に最後の点にはかなり大きなアドバンテージがあります。 MVCでは、ビューまたはビューコントローラーが表示用のデータの変換を担当する場合、このスレッドで発生する必要のあるUIへの実際の変更を、メインスレッドで行う必要があるものから分離するのは非常に難しいため、常にメインスレッドでこれを行います。その上で実行する必要はありません。また、UI変更以外のコードをメインスレッドで実行すると、アプリケーションの応答性が低下する可能性があります。

代わりに、このMVVMパターンでは、タップによってトリガーされたブロックから、ビューモデルが構築されてリスナーに渡されるまでのすべてが、別のスレッドで実行され、メインスレッドにのみディップできます。 UIの更新を終了します。アプリケーションがメインスレッドに費やす時間が少ない場合、アプリケーションはよりスムーズに実行されます。

ビューモデルが新しい状態をビューに適用すると、状態の別のレイヤーとして長引くのではなく、蒸発することができます。イベントをトリガーする可能性のあるものはすべてビュー内のアイテムに添付されており、ビューモデルに連絡することはありません。

覚えておくべき重要なことが1つあります。それは、ビューコントローラーを介してビューモデルをビューにマップする必要がないことです。前述のように、ビューの一部は、特に更新レートが異なる場合、他のビューモデルで管理できます。共同編集者がチャットペインを開いたまま、さまざまな人がGoogleスプレッドシートを編集していると考えてください。チャットメッセージが届くたびに、ドキュメントを更新することはあまり役に立ちません。

よく知られている例は、検索タイプの実装です。この実装では、テキストを入力すると、検索ボックスがより正確な結果で更新されます。これは、Stringでオートコンプリートを実装する方法です。クラス:画面全体がCreateAutocompleteViewによって提供されますしかし、テキストボックスはCreateViewModelをリッスンしています代わりに。

もう1つの例は、フォームバリデーターを使用することです。これは、「ローカルループ」(フィールドにエラー状態をアタッチまたは削除し、フォームが有効であると宣言する)として構築するか、イベントをトリガーすることで実行できます。

静的不変ビューモデルは、より優れた分離を提供します

静的なMVVM実装を使用することで、ビューモデルがモデルとビューの間を橋渡しするようになったため、最終的にすべてのレイヤーを完全に分離することができました。また、ユーザーの操作によって引き起こされたのではないイベントの管理を容易にし、アプリケーションのさまざまな部分間の多くの依存関係を削除しました。ビューコントローラが行う唯一のことは、受信したいイベントのリスナーとして、イベントハンドラに自分自身を登録(および登録解除)することです。

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)

) } ?? '' 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) } }
を登録することもできます。更新レートが異なる画面の一部がある場合は、リスナーとして直接。特定の会社に関するページの上部にある株式相場表示。

UIViewのコード非常に短いので、1ページもかかりません。残っているのは、ベースビューのオーバーライド、ナビゲーションバーからの追加ボタンの追加と削除、タイトルの設定、リスナーとしての自身の追加、およびChatsViewControllerの実装です。プロトコル:

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) } } のように、他の場所で実行できることは何も残っていません。最小限に抑えられます。

Swift MVVMチュートリアル:表示

不変のMVVMアーキテクチャのビューは、タスクのリストがまだあるため、依然としてかなり重い可能性がありますが、MVCアーキテクチャと比較して、次の責任を取り除くことができました。

特に最後の点にはかなり大きなアドバンテージがあります。 MVCでは、ビューまたはビューコントローラーが表示用のデータの変換を担当する場合、このスレッドで発生する必要のあるUIへの実際の変更を、メインスレッドで行う必要があるものから分離するのは非常に難しいため、常にメインスレッドでこれを行います。その上で実行する必要はありません。また、UI変更以外のコードをメインスレッドで実行すると、アプリケーションの応答性が低下する可能性があります。

代わりに、このMVVMパターンでは、タップによってトリガーされたブロックから、ビューモデルが構築されてリスナーに渡されるまでのすべてが、別のスレッドで実行され、メインスレッドにのみディップできます。 UIの更新を終了します。アプリケーションがメインスレッドに費やす時間が少ない場合、アプリケーションはよりスムーズに実行されます。

ビューモデルが新しい状態をビューに適用すると、状態の別のレイヤーとして長引くのではなく、蒸発することができます。イベントをトリガーする可能性のあるものはすべてビュー内のアイテムに添付されており、ビューモデルに連絡することはありません。

国境調整税とは

覚えておくべき重要なことが1つあります。それは、ビューコントローラーを介してビューモデルをビューにマップする必要がないことです。前述のように、ビューの一部は、特に更新レートが異なる場合、他のビューモデルで管理できます。共同編集者がチャットペインを開いたまま、さまざまな人がGoogleスプレッドシートを編集していると考えてください。チャットメッセージが届くたびに、ドキュメントを更新することはあまり役に立ちません。

よく知られている例は、検索タイプの実装です。この実装では、テキストを入力すると、検索ボックスがより正確な結果で更新されます。これは、Stringでオートコンプリートを実装する方法です。クラス:画面全体がCreateAutocompleteViewによって提供されますしかし、テキストボックスはCreateViewModelをリッスンしています代わりに。

もう1つの例は、フォームバリデーターを使用することです。これは、「ローカルループ」(フィールドにエラー状態をアタッチまたは削除し、フォームが有効であると宣言する)として構築するか、イベントをトリガーすることで実行できます。

静的不変ビューモデルは、より優れた分離を提供します

静的なMVVM実装を使用することで、ビューモデルがモデルとビューの間を橋渡しするようになったため、最終的にすべてのレイヤーを完全に分離することができました。また、ユーザーの操作によって引き起こされたのではないイベントの管理を容易にし、アプリケーションのさまざまな部分間の多くの依存関係を削除しました。ビューコントローラが行う唯一のことは、受信したいイベントのリスナーとして、イベントハンドラに自分自身を登録(および登録解除)することです。

利点:

欠点:

すばらしいのは、これが純粋なSwiftパターンであるということです。サードパーティのSwift MVVMフレームワークを必要とせず、従来のMVCの使用を排除しないため、今日のアプリケーションの新しい機能を簡単に追加したり、問題のある部分をリファクタリングしたりできます。アプリケーション全体を書き直すことを余儀なくされています。

より良い分離を提供する大きなビューコントローラーと戦うための他のアプローチもあります。それらを比較するためにすべてを詳細に含めることはできませんでしたが、いくつかの選択肢を簡単に見てみましょう。

従来のMVVMは、ほとんどのビューコントローラーコードを、単なる通常のクラスであり、単独でより簡単にテストできるビューモデルに置き換えます。ビューとモデルの間の双方向のブリッジである必要があるため、多くの場合、何らかの形式のObservableを実装します。そのため、RxSwiftなどのフレームワークと一緒に使用されることがよくあります。

MVPとVIPERは、より伝統的な方法でモデルとビューの間の追加の抽象化レイヤーを処理しますが、Reactiveは、データとイベントがアプリケーションを流れる方法を実際に再モデル化します。

この記事で説明されているように、リアクティブスタイルのプログラミングは最近多くの人気を博しており、実際にはイベントを使用した静的MVVMアプローチにかなり近いものです。主な違いは、通常はフレームワークが必要であり、コードの多くは特にそのフレームワークを対象としていることです。

MVPは、ViewControllerとViewの両方がViewLayerと見なされるパターンです。プレゼンターはモデルを変換してビューレイヤーに渡しますが、最初にデータをビューモデルに変換します。ビューはプロトコルに抽象化できるため、テストがはるかに簡単です。

VIPERは、MVPからプレゼンターを取得し、ビジネスロジック用に個別の「インタラクター」を追加し、モデルレイヤーを「エンティティ」と呼び、ナビゲーション用(および頭字語を完成させるため)のルーターを備えています。これは、MVPのより詳細で分離された形式と見なすことができます。


これで、静的なイベント駆動型MVVMについて説明しました。以下のコメントであなたからの連絡を楽しみにしています!

関連: Swiftチュートリアル:MVVMデザインパターンの概要

基本を理解する

MVVMの用途は何ですか?

ビューモデルは、ビューコントローラからすべてのロジックとモデルからビューへのコード(多くの場合、ビューからモデルへのバインディング)を引き継ぐ、独立したテストしやすいクラスです。

iOSのプロトコルとは何ですか?

プロトコル(他の言語では「インターフェイス」と呼ばれることが多い)は、任意のクラスまたは構造体で実装できる関数と変数のセットです。プロトコルは特定のクラスにバインドされていないため、実装されている限り、プロトコル参照に任意のクラスを使用できます。これにより、柔軟性が大幅に向上します。

iOSの委任パターンは何ですか?

デリゲートは、プロトコルに基づく別のクラスへの弱参照です。デリゲートは通常、タスクの完了後に、特定のクラスに縛られたり、そのすべての詳細を知らなくても、別のオブジェクトに「レポートバック」するために使用されます。

MVCとMVVMの違いは何ですか?

iOSでは、MVVMはMVCの代わりではなく、追加です。ビューコントローラは引き続き役割を果たしますが、ビューモデルはビューとモデルの中間になります。

iOSのMVPとは何ですか?

iOSでは、MVP(model-view-presenter)パターンは、UIViewsとUIViewControllersの両方がビューレイヤーの一部であるパターンです。 (紛らわしいことに、ビューレイヤーはアーキテクチャの概念ですが、UIViewはUIKitからのものであり、通常はビューとも呼ばれます。)