使用静态模式:Swift MVVM 教程
已发表: 2022-03-11今天,我们将看到用户对实时数据驱动应用程序的新技术可能性和期望如何给我们构建程序的方式带来新的挑战,尤其是我们的移动应用程序。 虽然本文是关于 iOS 和 Swift 的,但许多模式和结论同样适用于 Android 和 Web 应用程序。
在过去的几年里,现代移动应用程序的工作方式发生了重要变化。 由于更普遍的互联网访问和推送通知和 WebSockets 等技术,在当今的许多移动应用程序中,用户通常不再是运行时事件的唯一来源,也不一定是最重要的来源。
让我们仔细看看两种 Swift 设计模式在现代聊天应用程序中的表现如何:经典的模型-视图-控制器(MVC)模式和简化的不可变模型-视图-视图模型模式(MVVM,有时风格化为“ViewModel 模式” ”)。 聊天应用就是一个很好的例子,因为它们有许多数据源,并且需要在收到数据时以多种不同的方式更新其 UI。
我们的聊天应用程序
我们将在本 Swift MVVM 教程中用作指南的应用程序将具有我们从 WhatsApp 等聊天应用程序中了解的大部分基本功能。 让我们回顾一下我们将要实现的功能并比较 MVVM 和 MVC。 应用程序:
- 将从磁盘加载以前收到的聊天记录
- 将通过
GET请求与服务器同步现有聊天 - 当有新消息发送给用户时将收到推送通知
- 一旦我们进入聊天屏幕,将连接到 WebSocket
- 可以在聊天中
POST - 当收到关于我们当前不在的聊天的新消息时,将显示应用内通知
- 当我们收到当前聊天的新消息时,将立即显示新消息
- 当我们阅读未读消息时将发送已读消息
- 当有人阅读我们的消息时会收到一条已读消息
- 更新应用程序图标上的未读消息计数器徽章
- 将所有收到或更改的消息同步回 Core Data
在这个演示应用程序中,将没有真正的 API、WebSocket 或 Core Data 实现,以使模型实现更加简单。 相反,我添加了一个聊天机器人,一旦你开始对话,它就会开始回复你。 但是,如果存储和连接是真实的,则所有其他路由和调用的实现方式都是如此,包括返回之前的小的异步暂停。
已构建以下三个屏幕:
经典 MVC
首先,有用于构建 iOS 应用程序的标准 MVC 模式。 这是 Apple 构建其所有文档代码的方式,也是 API 和 UI 元素期望工作的方式。 这是大多数人在学习 iOS 课程时所学到的。
通常,MVC 被指责为导致几千行代码臃肿的UIViewController 。 但是如果应用得当,每一层之间有很好的分离,我们可以拥有非常纤细的ViewController ,它只充当View 、 Model和其他Controller之间的中间管理器。
这是应用程序的 MVC 实现的流程图(为了清楚起见,省略了CreateViewController ):
让我们详细了解这些层。
模型
模型层通常是 MVC 中问题最少的层。 在这种情况下,我选择使用ChatWebSocket 、 ChatModel和PushNotificationController在Chat和Message对象、外部数据源和应用程序的其余部分之间进行调解。 ChatModel是应用程序中的事实来源,并且仅在此演示应用程序中在内存中工作。 在现实生活中的应用程序中,它可能会得到 Core Data 的支持。 最后, ChatEndpoint处理所有 HTTP 调用。
看法
视图非常大,因为它必须处理很多职责,因为我已经小心地将所有视图代码与UIViewController分开。 我做了以下事情:
- 使用(非常推荐的)状态
enum模式来定义视图当前所处的状态。 - 添加了连接到按钮和其他触发动作的界面项目的功能(例如在输入联系人姓名时点击返回。)
- 设置约束并每次回调委托。
一旦你将UITableView放入混合中,视图现在比UIViewController大得多,导致令人担忧的 300 多行代码和ChatView中的许多混合任务。
控制器
由于所有模型处理逻辑都已移至ChatModel 。 所有的视图代码——可能潜伏在不太优化的分离项目中——现在都存在于视图中,因此UIViewController非常苗条。 视图控制器完全不知道模型数据的外观、获取方式或显示方式——它只是协调。 在示例项目中,没有一个UIViewController超过 150 行代码。
然而,ViewController 仍然做了以下事情:
- 作为视图和其他视图控制器的代表
- 如果需要,实例化和推送(或弹出)视图控制器
- 向
ChatModel发送和接收呼叫 - 根据视图控制器周期的阶段启动和停止 WebSocket
- 做出合乎逻辑的决定,例如如果消息为空则不发送消息
- 更新视图
这仍然很多,但主要是协调、处理回调块和转发。
好处
- 这种模式为大家所理解,由苹果公司推广
- 适用于所有文档
- 不需要额外的框架
缺点
- 视图控制器有很多任务; 其中很多基本上是在视图和模型层之间来回传递数据
- 不太适合处理多个事件源
- 班级往往对其他班级了解很多
问题定义
只要应用程序遵循用户的操作并对其做出响应,这就会非常有效,就像您想象的那样,Adobe Photoshop 或 Microsoft Word 之类的应用程序可以工作。 用户采取行动,UI 更新,重复。
但是现代应用程序是相互连接的,通常不止一种方式。 例如,您通过 REST API 进行交互,接收推送通知,在某些情况下,您还连接到 WebSocket。
这样一来,视图控制器突然需要处理更多的信息源,并且每当在没有用户触发的情况下接收到外部消息时(例如通过 WebSocket 接收消息),信息源都需要找到返回正确的路径视图控制器。 这需要大量代码将每个部分粘合在一起以执行基本相同的任务。
外部数据源
让我们看看当我们收到推送消息时会发生什么:
class PushNotificationController { class func received(notification: UNNotification, whenProcessed result: (_ shouldShow: Bool) -> Void) { let shouldShowNotification: Bool defer { result(shouldShowNotification) } let content = notification.request.content let date = DateParser.date(from: content.subtitle) ?? Date() let sender: Message.Sender = .other(name: content.title) let pushedMessage = Message(with: sender, message: content.body, state: .sent, sendDate: date) ChatModel.received(message: pushedMessage, by: content.title) if let chatViewController = chatViewController(handling: content.title) { chatViewController.received(message: pushedMessage) shouldShowNotification = false } else { shouldShowNotification = true } updateChats(for: content.title) } private static func updateChats(for contact: String) { guard let chat = ChatModel.loadedChats.first(where: { (chat) -> Bool in chat.contact == contact }) else { return assertionFailure("Chat for received message should always exist") } BaseNavigationViewController.navigationController?.viewControllers.forEach({ (viewController) in switch viewController { case let chatsViewController as UpdatedChatDelegate: chatsViewController.updated(chat: chat) default: break } }) } private static func chatViewController(handling contact: String) -> ChatViewController? { guard let lastViewController = BaseNavigationViewController.navigationController?.viewControllers.last as? ChatViewController, lastViewController.chat.contact == contact else { return nil } return lastViewController } } 我们必须手动挖掘视图控制器堆栈,以确定在我们收到推送通知后是否有需要更新自身的视图控制器。 在这种情况下,我们还想更新实现UpdatedChatDelegate的屏幕,在这种情况下,它只是ChatsViewController 。 我们也这样做是为了知道我们是否应该禁止通知,因为我们已经在Chat它的用途了。 在这种情况下,我们最终将消息传递给视图控制器。 很明显, PushNotificationController需要对应用程序了解太多才能完成其工作。
如果ChatWebSocket也将消息传递到应用程序的其他部分,而不是与ChatViewController建立一对一的关系,我们将在那里面临同样的问题。
很明显,每次我们添加另一个外部源时,我们都必须编写相当具有侵入性的代码。 这段代码也很脆弱,因为它严重依赖应用程序结构并将数据传递回层次结构以工作。
代表们
一旦我们添加了其他视图控制器,MVC 模式也会增加额外的复杂性。 这是因为视图控制器倾向于通过委托、初始化程序以及(在情节提要的情况下)传递数据和引用时的prepareForSegue来了解彼此。 每个视图控制器都处理自己与模型或中介控制器的连接,它们都在发送和接收更新。
此外,视图通过委托与视图控制器进行通信。 虽然这确实有效,但这意味着我们需要采取很多步骤来传递数据,而且我总是发现自己在回调和检查委托是否真的设置了很多。
可以通过更改另一个视图控制器中的代码来破坏一个视图控制器,例如ChatsListViewController中的陈旧数据,因为ChatViewController不再调用updated(chat: Chat) 。 尤其是在更复杂的场景中,让一切保持同步是一件很痛苦的事情。
视图和模型之间的分离
通过将所有与视图相关的代码从视图控制器移除到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中的日期:
func configure(with chat: Chat) { participant.text = chat.contact lastMessage.text = chat.messages.last?.message ?? "" lastMessageDate.text = chat.lastMessage.map { lastMessageDate in DateRenderer.string(from: lastMessageDate) } ?? "" show(unreadMessageCount: chat.unreadMessages) }即使在一个相当简单的示例中,也很明显在视图需要的内容和模型提供的内容之间存在张力。
静态事件驱动的 MVVM,又名静态事件驱动的“视图模型模式”
静态 MVVM 与视图模型一起工作,但不是通过它们创建双向流量——就像我们过去使用 MVC 通过视图控制器所做的那样——我们创建不可变的视图模型,每次 UI 需要更改以响应事件时更新 UI .
事件几乎可以由代码的任何部分触发,只要它能够提供事件enum所需的关联数据。 例如,接收received(new: Message)事件可以由推送通知、WebSocket 或常规网络调用触发。
让我们在图表中查看它:
乍一看,它似乎比经典的 MVC 示例复杂得多,因为要完成完全相同的事情涉及更多的类。 但仔细观察,这些关系都不再是双向的了。
更重要的是,对 UI 的每次更新都会由事件触发,因此对于发生的所有事情,只有一条通过应用程序的路径。 可以立即清楚您可以期待哪些事件。 如果需要,您应该在哪里添加新行为,或者在响应现有事件时添加新行为,这也很清楚。
重构之后,我得到了很多新的类,如上所示。 你可以在 GitHub 上找到我的静态 MVVM 版本的实现。 但是,当我将更改与cloc工具进行比较时,很明显实际上根本没有那么多额外的代码:
| 图案 | 文件 | 空白的 | 评论 | 代码 |
|---|---|---|---|---|
| MVC | 30 | 386 | 217 | 1807 |
| MVVM | 51 | 442 | 359 | 1981年 |
代码行数只增加了 9%。 更重要的是,这些文件的平均大小从 60 行代码下降到只有 39 行。
同样至关重要的是,最大的下降可以在 MVC 中通常最大的文件中找到:视图和视图控制器。 视图只有原始大小的 74%,而视图控制器现在只有原始大小的 53%。
还应该注意的是,许多额外的代码是库代码,它们有助于将块附加到可视树中的按钮和其他对象,而不需要 MVC 的经典@IBAction或委托模式。
让我们一一探索这个设计的不同层次。
事件
该事件始终是一个enum ,通常带有关联的值。 通常它们会与模型中的一个实体重叠,但不一定如此。 在这种情况下,应用程序分为两个主要事件enum : ChatEvent和MessageEvent 。 ChatEvent用于聊天对象本身的所有更新:
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 enum限制在合理的大小是很重要的。 如果您需要 10 个或更多案例,这通常表明您试图涵盖多个主题。
注意: enum概念在 Swift 中非常强大。 我倾向于经常使用带有关联值的enum ,因为它们可以消除您在使用可选值时可能会遇到的很多歧义。
Swift MVVM 教程:事件路由器
事件路由器是应用程序中发生的每个事件的入口点。 任何可以提供相关值的类都可以创建一个事件并将其发送到事件路由器。 因此它们可以由任何类型的来源触发,例如:
- 用户进入特定的视图控制器
- 用户点击某个按钮
- 应用程序启动
- 外部事件,例如:
- 网络请求返回失败或新数据
- 推送通知
- WebSocket 消息
事件路由器应该尽可能少地知道事件的来源,最好什么都不知道。 此示例应用程序中的所有事件都没有任何指示它们来自何处,因此很容易混合到任何类型的消息源中。 例如,WebSocket 触发相同的事件——received 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) } } } 这里几乎没有发生什么:我们唯一要做的就是更新模型并将事件转发到ChatEventHandler以便更新 UI。

Swift MVVM 教程:模型控制器
这与我们在 MVC 中使用的类完全相同,因为它已经运行得很好。 它代表应用程序的状态,通常由 Core Data 或本地存储库支持。
模型层——如果在 MVC 中正确实现——很少需要任何重构来适应不同的模式。 最大的变化是模型的更改发生在更少的类中,这使得更改发生的位置更加清晰。
在此模式的另一种选择中,您可以观察模型的变化并确保它们得到处理。 在这种情况下,我选择仅让*EventRouter和*Endpoint类更改模型,因此对模型的更新时间和地点有明确的责任。 相反,如果我们正在观察变化,我们将不得不编写额外的代码来通过ChatEventHandler传播非模型更改事件(如错误),这将使事件如何在应用程序中流动变得不那么明显。
Swift MVVM 教程:事件处理程序
事件处理程序是视图或视图控制器可以注册(和取消注册)自己作为侦听器以接收更新的视图模型的地方,这些视图模型是在ChatEventRouter调用ChatEventHandler上的函数时构建的。
可以看到,它大致反映了我们之前在 MVC 中使用的所有视图状态。 如果您想要其他类型的 UI 更新——比如声音或触发 Taptic 引擎——也可以从这里完成。
protocol ChatListListening: class { func updated(list: ChatListViewModel) } protocol CreateChatListening: class { func updated(create: CreateChatViewModel) } class ChatEventHandler { private static var chatListListening: [ChatListListening?] = [] private static var createChatListening: [CreateChatListening?] = [] class func add(listener: ChatListListening) { weak var weakListener = listener chatListListening.append(weakListener) } class func remove(listener: ChatListListening) { chatListListening = chatListListening.filter { $0 !== listener } } class func add(listener: CreateChatListening) { weak var weakListener = listener createChatListening.append(weakListener) listener.updated(create: CreateChatViewModelBuilder.build(isSending: false, error: nil)) } class func remove(listener: CreateChatListening) { createChatListening = createChatListening.filter { $0 !== listener } } class func started() { ChatEndpoint.fetchChats() let loadingViewModel = ChatListViewModelBuilder.buildLoading() chatListListening.forEach { $0?.updated(list: loadingViewModel) } } class func loaded(chats: [Chat]) { let chatList = ChatListViewModelBuilder.build(for: chats) chatListListening.forEach { $0?.updated(list: chatList) } } class func creatingChat() { let createChat = CreateChatViewModelBuilder.build(isSending: true, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } } class func failedCreatingChat(reason: String) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: reason) createChatListening.forEach { $0?.updated(create: createChat) } } class func created(chat: Chat) { let createChat = CreateChatViewModelBuilder.build(isSending: false, error: nil) createChatListening.forEach { $0?.updated(create: createChat) } updateAllChatLists() let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true, removePreviousFromStack: true) } class func updateAllChatLists() { let chatListViewModel = ChatListViewModelBuilder.build(for: ChatModel.allChats()) chatListListening.forEach { $0?.updated(list: chatListViewModel) } } } 此类仅确保在发生特定事件时正确的侦听器可以获得正确的视图模型。 如果需要设置初始状态,则新侦听器可以在添加时立即获得视图模型。 始终确保向列表添加weak引用以防止保留周期。
Swift MVVM 教程:查看模型
这是许多 MVVM 模式所做的与静态变体所做的最大区别之一。 在这种情况下,视图模型是不可变的,而不是将自己设置为模型和视图之间的永久双向绑定中间体。 我们为什么要这样做? 让我们停下来解释一下。
创建在所有可能情况下都能正常运行的应用程序的最重要方面之一是确保应用程序的状态正确。 如果 UI 与模型不匹配或有过时的数据,我们所做的一切都可能导致保存错误的数据或应用程序崩溃或以意外方式运行。
应用这种模式的目标之一是我们在应用程序中没有状态,除非它是绝对必要的。 究竟什么是状态? 状态基本上是我们存储特定类型数据表示的每个地方。 一种特殊类型的状态是您的 UI 当前所处的状态,当然我们无法通过 UI 驱动的应用程序来阻止这种状态。 其他类型的状态都是与数据相关的。 如果我们有一个Chat数组的副本在 Chat List 屏幕中备份我们的UITableView ,这就是重复状态的示例。 传统的双向绑定视图模型将是我们用户的Chat副本的另一个示例。
通过传递一个在每次模型更改时都会刷新的不可变视图模型,我们消除了这种类型的重复状态,因为在它应用到 UI 之后,它就不再被使用了。 然后我们只有两种我们无法避免的状态——UI 和模型——它们彼此完美同步。
所以这里的视图模型与一些 MVVM 应用程序有很大的不同。 它仅用作视图反映模型状态所需的所有标志、值、块和其他值的不可变数据存储,但视图不能以任何方式更新它。
因此它可以是一个简单的不可变struct 。 为了使这个struct尽可能简单,我们将使用视图模型构建器对其进行实例化。 视图模型的有趣之处之一是它获得了像shouldShowBusy和shouldShowError这样的行为标志,它们替换了之前在视图中找到的状态enum机制。 这是我们之前分析过的ChatItemTableViewCell的数据:
struct ChatListItemViewModel { let contact: String let message: String let lastMessageDate: String let unreadMessageCount: Int let itemTapped: () -> Void }因为视图模型构建器已经处理了视图需要的确切值和操作,所以所有数据都是预先格式化的。 同样新的是一个块,一旦点击一个项目就会触发。 让我们看看它是如何由视图模型构建器制作的。
查看模型生成器
视图模型构建器可以构建视图模型的实例,将诸如Chat或Message之类的输入转换为为特定视图完美定制的视图模型。 在视图模型构建器中发生的最重要的事情之一是确定视图模型中块内部实际发生的事情。 视图模型构建器附加的块应该非常短,尽快调用架构其他部分的功能。 这样的块不应该有任何业务逻辑。
class ChatListItemViewModelBuilder { class func build(for chat: Chat) -> ChatListItemViewModel { let lastMessageText = chat.messages.last?.message ?? "" let lastMessageDate = (chat.messages.last?.sendDate).map { DateRenderer.string(from: $0) } ?? "" let unreadMessageCount = ChatModel.unreadMessages(for: chat.contact).count return ChatListItemViewModel(contact: chat.contact, message: lastMessageText, lastMessageDate: lastMessageDate, unreadMessageCount: unreadMessageCount, itemTapped: { show(chat: chat) }) } private class func show(chat: Chat) { let chatViewController = ChatViewController(for: chat) BaseNavigationViewController.pushViewController(chatViewController, animated: true) } }现在所有的预格式化都发生在同一个地方,行为也在这里决定。 它是这个层次结构中非常重要的一个类,看看演示应用程序中的不同构建器是如何实现的并处理更复杂的场景可能会很有趣。
Swift MVVM 教程:视图控制器
这种架构中的视图控制器做的很少。 它将设置和拆除与其视图相关的所有内容。 这样做是最合适的,因为它会获取在正确的时间添加和删除侦听器所需的所有生命周期回调。
有时它需要更新根视图未覆盖的 UI 元素,例如导航栏中的标题或按钮。 这就是为什么如果我有一个覆盖给定视图控制器的整个视图的视图模型,我通常仍然将视图控制器注册为事件路由器的侦听器; 之后我将视图模型转发到视图。 但是,如果屏幕的某个部分具有不同的更新率,也可以直接将任何UIView注册为侦听器,例如有关某个公司的页面顶部的实时股票行情。
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) } } 由于ChatsViewController已被剥离到最低限度,因此无法在其他地方完成任何事情。
Swift MVVM 教程:查看
不可变 MVVM 架构中的视图仍然非常繁重,因为它仍然有一个任务列表,但与 MVC 架构相比,我设法将其剥离了以下职责:
- 确定响应新状态需要更改的内容
- 为动作实现委托和函数
- 处理手势和触发动画等视图到视图触发器
- 以可以显示的方式转换数据(如
Date到String)
尤其是最后一点有相当大的优势。 在 MVC 中,当视图或视图控制器负责转换数据以供显示时,它将始终在主线程上执行此操作,因为很难将需要在该线程上发生的 UI 的真正更改与实际发生的事情区分开来不需要在上面运行。 并且在主线程上运行非 UI 更改代码会导致应用程序响应速度降低。
相反,使用这种 MVVM 模式,从由点击触发的块到构建视图模型并将其传递给侦听器的所有内容 - 我们可以在单独的线程上运行这一切,并且只进入主线程结束做 UI 更新。 如果我们的应用程序在主线程上花费的时间更少,它将运行得更顺畅。
一旦视图模型将新状态应用到视图,它就可以消失而不是作为另一层状态徘徊。 可能触发事件的所有内容都附加到视图中的项目,我们不会与视图模型进行通信。
重要的是要记住一件事:您不必通过视图控制器将视图模型映射到视图。 如前所述,视图的某些部分可以由其他视图模型管理,尤其是在更新率不同时。 考虑一个由不同的人编辑的 Google 表格,同时保持一个聊天窗格为协作者打开——每当有聊天消息到达时刷新文档并不是很有用。
一个著名的例子是类型查找实现,当我们输入更多文本时,搜索框会更新为更准确的结果。 这就是我在CreateAutocompleteView类中实现自动完成的方式:整个屏幕由CreateViewModel提供服务,但文本框正在监听AutocompleteContactViewModel 。
另一个例子是使用表单验证器,它可以构建为“本地循环”(将错误状态附加或删除到字段并声明表单有效)或通过触发事件来完成。
静态不可变视图模型提供更好的分离
通过使用静态 MVVM 实现,我们最终成功地完全分离了所有层,因为视图模型现在在模型和视图之间架起了桥梁。 我们还让管理不是由用户操作引起的事件变得更容易,并消除了我们应用程序不同部分之间的大量依赖关系。 视图控制器唯一要做的就是将自己注册(和注销)到事件处理程序,作为它想要接收的事件的侦听器。
好处:
- 视图和视图控制器的实现往往要轻得多
- 类更加专业化和分离
- 可以从任何地方轻松触发事件
- 事件遵循可预测的路径通过系统
- 状态仅从一个地方更新
- 应用程序可以提高性能,因为它更容易在主线程之外工作
- 视图接收量身定制的视图模型,并与模型完美分离
缺点:
- A full view model is created and sent every time the UI needs to update, often overwriting the same button text with the same button text, and replacing blocks with blocks that do exactly the same
- Requires some helper extensions to make button taps and other UI events work well with the blocks in the view model
- Event
enums can easily grow pretty large in complex scenarios and might be hard to split up
The great thing is that this is a pure Swift pattern: It does not require a third-party Swift MVVM framework, nor does it exclude the use of classic MVC, so you can easily add new features or refactor problematic parts of your application today without being forced to rewrite your whole application.
There are other approaches to combat large view controllers that provide better separation as well. I couldn't include them all in full detail to compare them, but let's take a brief look at some of the alternatives:
- Some form of the MVVM pattern
- Some form of Reactive (using RxSwift, sometimes combined with MVVM)
- The model-view-presenter pattern (MVP)
- The view-interactor-presenter-entity-router pattern (VIPER)
Traditional MVVM replaces most of the view controller code with a view model that is just a regular class and can be tested more easily in isolation. Since it needs to be a bi-directional bridge between the view and the model it often implements some form of Observables. That's why you often see it used together with a framework like RxSwift.
MVP and VIPER deal with extra abstraction layers between the model and the view in a more traditional way, while Reactive really remodels the way data and events flow through your application.
The Reactive style of programming is gaining a lot of popularity lately and actually is pretty close to the static MVVM approach with events, as explained in this article. The major difference is that it usually requires a framework, and a lot of your code is specifically geared towards that framework.
MVP is a pattern where both the view controller and the view are considered to be the view layer. The presenter transforms the model and passes it to the view layer, while I transform the data into a view model first. Since the view can be abstracted to a protocol, it's much easier to test.
VIPER takes the presenter from MVP, adds a separate “interactor” for business logic, calls the model layer “entity,” and has a router for navigation purposes (and to complete the acronym). It can be considered a more detailed and decoupled form of MVP.
So there you have it: static event-driven MVVM explained. I look forward to hearing from you in the comments below!
