个性化阅读
专注于IT技术分析

使用静态模式:Swift MVVM教程

本文概述

今天, 我们将了解用户对实时数据驱动的应用程序的新技术可能性和期望如何在我们构建程序(尤其是移动应用程序)的方式上带来新的挑战。尽管本文是关于iOS和Swift的, 但许多模式和结论同样适用于Android和Web应用程序。

在过去的几年中, 现代移动应用的工作方式有了重要的发展。得益于更广泛的Internet访问以及推送通知和WebSocket等技术, 用户通常不再是当今许多移动应用程序中运行时事件的唯一来源, 也不再是最重要的运行时事件。

让我们仔细研究一下两种Swift设计模式在现代聊天应用程序中的工作情况:经典的模型-视图-控制器(MVC)模式和简化的不可变模型-视图-视图模型模式(MVVM, 有时是风格化的” ViewModel”模式”)。聊天应用程序就是一个很好的例子, 因为它们有许多数据源, 并且每当接收到数据时都需要以许多不同的方式更新其UI。

我们的聊天应用

我们将在本Swift MVVM教程中用作指南的应用程序将具有我们从诸如WhatsApp之类的聊天应用程序中了解到的大多数基本功能。让我们看一下我们将实现的功能, 并比较MVVM和MVC。应用程序:

  • 将从磁盘加载以前收到的聊天
  • 将通过GET请求将现有聊天与服务器同步
  • 将新消息发送给用户时将接收推送通知
  • 进入聊天屏幕后, 将连接到WebSocket
  • 可以将新消息发布到聊天中
  • 当收到我们当前不在的聊天消息时, 将显示应用内通知
  • 当我们收到当前聊天的新消息时, 将立即显示新消息
  • 当我们阅读未读邮件时将发送已读邮件
  • 当有人阅读我们的信息时, 将收到已阅读的信息
  • 更新应用程序图标上的未读邮件计数器徽章
  • 将收到或更改的所有消息同步回核心数据

在此演示应用程序中, 将没有真正的API, WebSocket或Core Data实现来使Model实现更加简单。相反, 我添加了一个聊天机器人, 一旦你开始对话, 它就会开始回复你。但是, 所有其他路由和调用的实现方式都与存储和连接是真实的一样, 包括返回之前的小的异步暂停。

构建了以下三个屏幕:

聊天列表,创建聊天和消息屏幕。

经典MVC

首先, 有用于构建iOS应用程序的标准MVC模式。这就是Apple构造其所有文档代码以及API和UI元素期望工作的方式。这是大多数人在上iOS课程时所学的知识。

MVC通常会导致导致数千行代码的UIViewControllers膨胀。但是, 如果应用得当, 并且各层之间的分隔良好, 我们可以拥有相当纤细的ViewController, 它们的作用就像在View, Model和其他Controller之间的中间管理器一样。

这是该应用的MVC实现的流程图(为清楚起见, 省略了CreateViewController):

MVC实现流程图,为清晰起见,省略了CreateViewController。

让我们详细介绍一下各个层。

模型

模型层通常是MVC中问题最少的层。在这种情况下, 我选择使用ChatWebSocket, ChatModel和PushNotificationController在Chat和Message对象, 外部数据源和应用程序的其余部分之间进行中介。 ChatModel是应用程序中真相的来源, 并且仅在此演示应用程序的内存中工作。在现实生活中的应用程序中, 它可能会得到Core Data的支持。最后, ChatEndpoint处理所有HTTP调用。

视图

由于我已经将所有视图代码与UIViewControllers仔细分离, 因此视图非常大, 因为它必须承担很多责任。我已经完成以下工作:

  • 使用(非常值得推荐的)状态枚举模式来定义视图当前所处的状态。
  • 添加了与按钮和其他触发动作的界面项相关的功能(例如在输入联系人姓名时点击”返回”)。
  • 设置约束并每次都回调给委托人。

一旦将UITableView放入混合中, 视图现在将比UIViewControllers大得多, 从而导致令人担忧的300多行代码以及ChatView中的许多混合任务。

控制者

由于所有模型处理逻辑都已转移到ChatModel。现在, 所有视图代码(可能会在不太理想的, 分离的项目中潜伏在这里)现在都位于视图中, 因此UIViewControllers非常苗条。视图控制器完全忽略了模型数据的外观, 如何获取或应如何显示(它们只是坐标)。在示例项目中, 没有UIViewControllers超过150行代码。

但是, ViewController仍会执行以下操作:

  • 成为视图和其他视图控制器的委托
  • 如果需要, 实例化和推送(或弹出)视图控制器
  • 发送和接收来自ChatModel的呼叫
  • 根据视图控制器周期的阶段启动和停止WebSocket
  • 做出合理的决定, 例如如果消息为空则不发送消息
  • 更新视图

仍然很多, 但主要是协调, 处理回调块和转发。

好处

  • 这种模式为每个人所了解, 并由Apple推广
  • 适用于所有文档
  • 无需额外的框架

缺点

  • 视图控制器有很多任务。他们中的很多基本上是在视图和模型层之间来回传递数据
  • 不太适合处理多个事件源
  • 班级往往对其他班级了解很多

问题定义

只要应用程序遵循用户的操作并对其做出响应, 此方法就可以很好地工作, 就像你想象的那样, 像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。我们也这样做是为了知道是否应该取消通知, 因为我们已经在查看它的目的是聊天。在这种情况下, 我们最终将消息传递给视图控制器。很明显, PushNotificationController需要对应用程序了解太多, 才能完成其工作。

如果ChatWebSocket也将消息传递到应用程序的其他部分, 而不是与ChatViewController具有一对一的关系, 那么我们在那里将面临同样的问题。

显然, 每次添加其他外部资源时, 我们都必须编写具有侵入性的代码。该代码也非常脆弱, 因为它严重依赖于应用程序结构, 并委托将数据传回到层次结构中才能正常工作。

代表们

一旦我们添加了其他视图控制器, MVC模式也将添加额外的复杂性。这是因为视图控制器在传递数据和引用时, 往往会通过委托, 初始化器, 以及(对于情节提要)通过prepareForSegue相互了解。每个视图控制器都处理自己与模型或中介控制器的连接, 并且它们都在发送和接收更新。

同样, 视图通过委托传递回视图控制器。尽管这样做确实可行, 但这意味着我们需要采取许多步骤来传递数据, 而且我总是发现自己围绕回调进行了大量重构, 并检查委托是否已真正设置。

可以通过更改另一个视图中的代码来破坏一个视图控制器, 例如ChatsListViewController中的陈旧数据, 因为ChatViewController不再调用updated(聊天)。特别是在更复杂的情况下, 要使所有内容保持同步是很痛苦的。

视图与模型之间的分离

通过将所有与视图相关的代码从视图控制器删除到customViews, 并将所有与模型相关的代码移动到专用控制器, 视图控制器非常精简和分离。但是, 仍然存在一个问题:视图要显示的内容与模型中的数据之间存在差距。一个很好的例子是ChatListView。我们要显示的是一个单元格列表, 该单元格告诉我们我们正在与谁聊天, 最后一条消息是什么, 最后一条消息的日期以及聊天室中还剩下多少未读消息:

聊天屏幕中的未读邮件计数器。

但是, 我们正在传递的模型并不知道我们想要看到什么。相反, 它只是一个包含联系人的聊天, 其中包含以下消息:

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

现在可以快速添加一些额外的代码, 这些代码将使我们获得最后一条消息和消息数, 但是将日期格式化为字符串是一项完全属于视图层的任务:

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

所以最后, 当我们显示日期时, 我们在ChatItemTableViewCell中格式化日期:

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

即使在一个非常简单的示例中, 也很明显在视图需要和模型提供之间存在压力。

静态事件驱动的MVVM, 又称静态事件驱动的” ViewModel模式”

静态MVVM可与视图模型一起使用, 但与其创建双向流量(就像我们以前使用MVC的视图控制器一样), 我们创建了不可变的视图模型, 每当需要响应事件而更改UI时, 该视图模型都会更新UI。 。

只要事件能够提供事件枚举所需的关联数据, 事件几乎可以由代码的任何部分触发。例如, 可以通过推送通知, WebSocket或常规网络调用来触发接收到的(新的:消息)事件。

让我们在图中查看它:

MVVM实施流程图。

乍一看, 它似乎比经典的MVC示例要复杂得多, 因为要完成完全相同的事情需要涉及更多的类。但是仔细检查, 这些关系不再是双向的。

更为重要的是, 对UI的每次更新都会由事件触发, 因此对于发生的所有事情, 只有一条通过应用程序的路线。立即清楚你可以预期的事件。同样清楚的是, 如果需要, 你应该在哪里添加新的, 或者在响应现有事件时添加新的行为。

重构后, 如上所示, 我最终获得了许多新类。你可以在GitHub上找到我对静态MVVM版本的实现。但是, 当我将更改与cloc工具进行比较时, 很明显, 实际上根本没有那么多额外的代码:

图案 档案 空白 注释 代码
MVC 30 386 217 1807
MVVM 51 442 359 1981

代码行仅增加了9%。更重要的是, 这些文件的平均大小从60行代码减少到只有39行。

代码行饼图。视图控制器:MVC 287和MVVM 154相比降低了47%;观看次数:MVC 523和MVVM 392相比减少了26%。

同样至关重要的是, 在MVC中通常最大的文件:视图和视图控制器中可以找到最大的下降。视图仅占其原始大小的74%, 而视图控制器现在仅占其原始大小的53%。

还要注意的是, 许多额外的代码是库代码, 可以帮助将块附加到可视树中的按钮和其他对象, 而无需MVC的经典@IBAction或委托模式。

让我们一步一步地探索该设计的不同层面。

事件

该事件始终是一个枚举, 通常带有关联的值。它们通常会与模型中的一个实体重叠, 但不一定如此。在这种情况下, 应用程序分为两个主要事件枚举:ChatEvent和MessageEvent。 ChatEvent用于聊天对象本身的所有更新:

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

另一个处理所有与Message相关的事件:

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枚举限制为合理的大小非常重要。如果你需要10个以上的案例, 通常这是你尝试涵盖多个主题的标志。

注意:枚举概念在Swift中非常强大。我倾向于大量使用带有关联值的枚举, 因为它们可以消除很多其他情况下可选值所带来的歧义。

Swift MVVM教程:事件路由器

事件路由器是应用程序中发生的每个事件的入口点。任何可以提供相关值的类都可以创建一个事件并将其发送到事件路由器。因此它们可以由任何种类的来源触发, 例如:

  • 用户选择进入特定的视图控制器
  • 用户点击某个按钮
  • 申请开始
  • 外部事件, 例如:
    • 网络请求返回失败或新数据
    • 推送通知
    • WebSocket消息

事件路由器应该对事件的来源尽可能少的了解, 最好一点也不了解。此示例应用程序中的所有事件都没有任何指示器来指示它们的来源, 因此很容易混入任何类型的消息源。例如, WebSocket触发相同的事件-接收到的(消息:消息, 联系人:字符串)-作为新的推送通知。

事件(你已经猜到了)被路由到需要进一步处理这些事件的类。通常, 仅调用的类是模型层(如果需要添加, 更改或删除数据)和事件处理程序。我将在后面再进行讨论, 但是事件路由器的主要功能是为所有事件提供一个简单的访问点, 并将工作转发给其他班级。这里以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) }
    }
}

该类只需要确保适当的侦听器可以在发生特定事件时获取正确的视图模型即可。如果新侦听器需要设置其初始状态, 则可以在添加后立即获得视图模型。始终确保为列表添加一个弱引用, 以防止保留周期。

Swift MVVM教程:视图模型

这是许多MVVM模式与静态变量之间的最大区别之一。在这种情况下, 视图模型是不可变的, 而不是将自身设置为模型和视图之间的永久性双向绑定中间对象。我们为什么要这样做?让我们暂停一下, 片刻。

创建在所有可能的情况下都能正常运行的应用程序的最重要方面之一就是确保应用程序的状态正确。如果用户界面与模型不匹配或数据过时, 我们所做的一切都可能导致错误数据被保存或应用程序意外崩溃或行为异常。

应用此模式的目的之一是, 除非绝对必要, 否则应用程序中没有状态。到底是什么状态?状态基本上是我们存储特定类型数据表示的每个地方。一种特殊的状态类型是你的UI当前所处的状态, 当然, 对于UI驱动的应用程序我们无法避免这种状态。其他类型的状态都与数据相关。如果我们有一个聊天数组的副本来备份”聊天列表”屏幕中的UITableView, 那就是重复状态的一个示例。传统的双向视图模型将是我们的用户聊天记录重复的另一个示例。

通过传递一个不变的视图模型, 该模型在每次模型更改时都会刷新, 因此, 我们消除了这种重复状态, 因为在将其自身应用于UI后, 就不再使用了。然后, 我们只有两种无法避免的状态-UI和模型-它们彼此之间完全同步。

因此, 这里的视图模型与某些MVVM应用程序完全不同。它仅用作视图反映模型状态所需的所有标志, 值, 块和其他值的不变数据存储, 但视图无法以任何方式对其进行更新。

因此, 它可以是一个简单的不变结构。为了使该结构尽可能简单, 我们将使用视图模型构建器对其进行实例化。关于视图模型的有趣的事情之一是, 它获得了行为标记, 如shouldShowBusy和shouldShowError, 它们替换了先前在视图中发现的状态枚举机制。以下是我们之前分析过的ChatItemTableViewCell的数据:

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

由于视图模型构建器已经处理了视图所需的确切值和操作, 因此所有数据都已预先格式化。还有一个新的功能是, 一旦点击一个物品就会触发一个方块。让我们看看视图模型构建器是如何制作的。

查看模型生成器

视图模型构建器可以构建视图模型的实例, 将诸如”聊天”或”消息”之类的输入转换为针对特定视图完美定制的视图模型。在视图模型构建器中发生的最重要的事情之一就是确定视图模型的块内部实际发生了什么。视图模型构建器附加的块应非常短, 应尽快调用体系结构其他部分的功能。此类块不应具有任何业务逻辑。

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架构相比, 我设法将其剥离了以下职责:

  • 确定响应新状态需要更改的内容
  • 实现行动的代表和职能
  • 处理诸如手势和触发动画之类的视图到视图触发器
  • 以可以显示数据的方式转换数据(例如, 日期转换为字符串)

特别是最后一点具有很大的优势。在MVC中, 当视图或视图控制器负责转换数据以进行显示时, 它将始终在主线程上执行此操作, 因为很难将真正发生在该线程上的UI更改与不需要在其上运行。并且在主线程上运行非UI更改代码可能导致应用程序响应速度较慢。

取而代之的是, 使用这种MVVM模式, 从轻敲触发的块直到构建视图模型的那一刻, 所有内容都将传递给侦听器, 我们可以在单独的线程上运行所有这些, 而仅将其浸入主线程中。完成UI更新。如果我们的应用程序在主线程上花费的时间更少, 它将运行得更流畅。

视图模型将新状态应用于视图后, 它便会蒸发掉, 而不再是另一层状态。可能触发事件的所有内容都附加在视图中的项目上, 我们不会与视图模型进行通讯。

需要记住的一件事很重要:不必强迫你通过视图控制器将视图模型映射到视图。如前所述, 视图的某些部分可以由其他视图模型管理, 尤其是在更新速率变化时。假设Google表格是由不同的人编辑的, 同时让协作者可以打开聊天窗格-每当聊天消息到来时刷新文档都没有太大用处。

一个著名的例子是一种类型查找方法, 其中当我们输入更多文本时, 搜索框将更新为更准确的结果。这就是我在CreateAutocompleteView类中实现自动完成的方式:整个屏幕由CreateViewModel提供服务, 但是文本框改为侦听AutocompleteContactViewModel。

另一个例子是使用表单验证器, 它可以构建为”本地循环”(将错误状态附加或删除到字段并声明表单有效)或通过触发事件来完成。

静态不可变视图模型提供了更好的分离

通过使用静态MVVM实施, 由于视图模型现在在模型和视图之间架起了桥梁, 因此我们最终设法将所有层完全分开。我们还使管理非用户操作引起的事件变得更加容易, 并消除了应用程序不同部分之间的许多依赖关系。视图控制器唯一要做的就是将自身注册(并注销)到事件处理程序中, 作为其想要接收的事件的侦听器。

好处:

  • 视图和视图控制器的实现往往要轻得多
  • 班级更专业且分开
  • 可以从任何地方轻松触发事件
  • 事件遵循可预测的系统路径
  • 状态仅从一个地方更新
  • 应用程序性能更高, 因为更容易在主线程上完成工作
  • 视图接收量身定制的视图模型, 并且与模型完美分离

缺点:

  • 每当用户界面需要更新时, 都会创建并发送完整视图模型, 通常会用相同的按钮文本覆盖相同的按钮文本, 并用功能完全相同的块替换块
  • 需要一些帮助程序扩展, 才能使按钮点击和其他UI事件与视图模型中的块一起正常工作
  • 在复杂的情况下, 事件枚举很容易变得很大, 并且可能很难拆分

很棒的是, 这是一个纯粹的Swift模式:它不需要第三方Swift MVVM框架, 也不排除使用经典MVC, 因此你现在可以轻松添加新功能或重构应用程序中有问题的部分而无需被迫重写整个应用程序。

还有其他与大视图控制器对抗的方法也可以提供更好的分离。我无法全部包含它们以进行比较, 但是让我们简要地看一下其中一些替代方法:

  • 某种形式的MVVM模式
  • 某种形式的反应式(使用RxSwift, 有时与MVVM结合使用)
  • 模型视图呈现者模式(MVP)
  • 视图交互者表示者实体路由器模式(VIPER)

传统的MVVM用只是常规类的视图模型替换了大多数视图控制器代码, 并且可以更轻松地进行隔离测试。由于它需要在视图和模型之间建立双向桥梁, 因此它通常实现某种形式的Observable。这就是为什么你经常看到它与RxSwift之类的框架一起使用的原因。

MVP和VIPER以更传统的方式处理模型和视图之间的额外抽象层, 而Reactive则真正重塑了数据和事件在应用程序中流动的方式。

响应式编程风格最近变得越来越流行, 并且实际上非常类似于带有事件的静态MVVM方法, 如本文所述。主要区别在于它通常需要一个框架, 并且你的许多代码专门针对该框架。

MVP是一种模式, 其中视图控制器和视图都被视为视图层。演示者转换模型并将其传递到视图层, 而我首先将数据转换为视图模型。由于可以将视图抽象为协议, 因此测试起来非常容易。

VIPER从MVP中提取演示者, 为业务逻辑添加了单独的”交互器”, 将模型层称为”实体”, 并具有用于导航目的的路由器(并用于完成首字母缩写词)。可以认为它是MVP的更详细和分离的形式。


这样就可以了:解释了静态事件驱动的MVVM。我期待在下面的评论中收到你的来信!

相关:Swift教程:MVVM设计模式简介

赞(0)
未经允许不得转载:srcmini » 使用静态模式:Swift MVVM教程

评论 抢沙发

评论前必须登录!