作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Lucas van Dongen's profile image

Lucas van Dongen

A mobile developer 和 teacher, Lucas不仅开发过iOS应用,还使用斯威夫特开发过后端api, Objective-C, Python, Elixir, C#.

Years of Experience

16

分享

今天,我们将看到用户对实时数据驱动应用程序的新技术可能性和期望如何给我们构建程序的方式带来新的挑战, especially our mobile applications. While this article is about iOS斯威夫特,许多模式和结论同样适用于Android和web应用程序.

在过去几年里,现代移动应用的工作方式发生了重大变化. 多亏了更普遍的互联网接入和技术,比如 push notifications 和 WebSockets, 在今天的许多移动应用中,用户通常不再是运行时事件的唯一来源,也不一定是最重要的来源.

让我们仔细看看两种斯威夫特设计模式是如何与现代聊天应用程序一起工作的:经典的模型-视图-控制器(MVC)模式和简化的不可变模型-视图-视图模型(MVVM)模式, 有时是程式化的“视图模型模式”). 聊天应用程序就是一个很好的例子,因为它们有许多数据源,并且需要在接收到数据时以许多不同的方式更新其ui.

Our 闲谈,聊天 Application

我们将在这个斯威夫特 MVVM教程中用作指导的应用程序将具有我们从WhatsApp等聊天应用程序中知道的大多数基本功能. 让我们回顾一下我们将要实现的特性,并比较MVVM和MVC. The application:

  • 将从磁盘加载以前收到的聊天记录吗
  • Will sync the existing 闲谈,聊天s over a 得到-request with the server
  • 当新消息发送给用户时,会收到推送通知吗
  • 将连接到WebSocket一旦我们在聊天屏幕
  • 可以 帖子 a new message to a 闲谈,聊天
  • 会显示一个应用程序内的通知,当收到一个新的消息,我们目前不在聊天
  • 当我们收到当前聊天的新消息时,会立即显示新消息吗
  • 当我们读取未读消息时会发送已读消息吗
  • 当有人读到我们的信息时,会收到一条已读信息吗
  • 更新应用程序图标上的未读消息计数器标记
  • 将所有收到或更改的消息同步回核心数据

In this demo application, there will be no real API, WebSocket, 或Core Data实现,以使模型实现更简单一些. 相反,我添加了一个聊天机器人,一旦你开始对话,它就会开始回复你. However, 如果存储和连接是真实的,那么所有其他路由和调用都将像它们一样实现, 包括返回前的小异步暂停.

建立了以下三个屏幕:

“聊天列表”、“创建聊天”和“消息”界面.

Classic MVC

首先,有用于构建iOS应用程序的标准MVC模式. 这是苹果构建其所有文档代码的方式,也是api和UI元素预期的工作方式. 这是大多数人在参加iOS课程时学到的内容.

MVC经常被指责为导致臃肿 UI视图Controllers of a few thous和 lines of code. 但如果使用得当,每一层之间都能很好地分离,我们就能做得很薄 视图Controller他们只扮演中间管理者的角色 视图s, 模型s, other Controllers.

Here’s the flowchart for the MVC implementation of the app (leaving out the Create视图Controller for clarity):

MVC实现流程图,为了清晰起见,省略Create视图Controller.

Let’s go over the layers in detail.

模型

模型层通常是MVC中问题最少的一层. In this 情况下, I opted to use 闲谈,聊天WebSocket, 闲谈,聊天模型, PushNotificationController to mediate between the 闲谈,聊天Message 对象、外部数据源和应用程序的其余部分. 闲谈,聊天模型 在这个演示应用程序中,真实源是否在应用程序中并且只在内存中工作. 在实际应用中,它可能由Core Data提供支持. Lastly, 闲谈,聊天Endpoint h和les all HTTP calls.

视图

视图非常大,因为它必须处理很多责任,因为我已经仔细地将所有视图代码从 UI视图Controllers. I’ve done the following:

  • Used the (very recommendable) 状态 枚举 pattern 定义视图当前所处的状态.
  • 添加了连接到按钮和其他动作触发界面项的功能(如在输入联系人姓名时点击Return).)
  • 设置约束并每次回调委托.

Once you throw a UITable视图 在这个组合中,视图现在比 UI视图ControllerS,导致令人担忧的300多行代码和大量混合任务 闲谈,聊天视图.

Controller

因为所有的模型处理逻辑都转移到了 闲谈,聊天模型. 所有的视图代码,可能会隐藏在这里,不太理想, 分离的项目—现在位于视图中, so the UI视图Controllers are pretty slim. 视图控制器完全忽略了模型数据的样子, how it is fetched, 或者它应该如何显示——它只是坐标. In the example project, none of the UI视图Controllers goes over 150 lines of code.

然而,视图Controller仍然做以下事情:

  • 作为视图和其他视图控制器的委托
  • 如果需要,实例化和推送(或弹出)视图控制器
  • 发送和接收电话到和从 闲谈,聊天模型
  • 根据视图控制器周期的阶段来启动和停止WebSocket
  • 做出合理的决定,比如如果信息是空的就不发送
  • Updating the view

这仍然很多,但它主要是协调、处理回调块和转发.

Benefits

  • 这种模式大家都懂,苹果也在推广
  • Works with all documentation
  • No extra frameworks needed

Downsides

  • 视图 controllers have a lot of tasks; a lot of them are basically passing data back 和 forth between the view 和 the model layer
  • 不太适合处理多个事件源
  • 班级往往对其他班级了解很多

Problem Definition

只要应用程序遵循用户的操作并对其做出响应,这种方法就能很好地工作, 就像你想象的Adobe Photoshop或Microsoft Word这样的应用程序一样. 用户采取一个操作,UI更新,重复.

但现代应用程序之间的联系往往不止一种方式. For example, you interact through a REST API, receive push notifications, in some 情况下s, you connect to a WebSocket as well.

With that, 突然间,视图控制器需要处理更多的信息源, 无论何时,在没有用户触发的情况下接收到外部消息(比如通过websocket接收消息),信息源都需要找到返回正确视图控制器的方法. 这需要大量的代码来将每个部分粘合在一起以执行基本相同的任务.

External Data Sources

让我们看看当我们收到推送消息时会发生什么:

class PushNotificationController {
    类函数received(notification: UNNotification)
            whenProcessed result: (_ shouldShow: Bool) -> Void) {
        let shouldShowNotification: Bool
        defer {
            result(shouldShowNotification)
        }
        let content = notification.request.content
        let date = 日期Parser.date(from: content.subtitle) ?? 日期()
        let sender: Message.Sender = .other(name: content.title)
        让pushedMessage = Message(with: sender, Message: content).body, 状态: .sent, send日期: date)
        闲谈,聊天模型.received(message: pushedMessage, by: content).title)
        如果让闲谈,聊天视图Controller = 闲谈,聊天视图Controller(h和ling: content.title) {
            闲谈,聊天视图Controller.received(message: pushedMessage)
            shouldShowNotification = false
        } else {
            shouldShowNotification = true
        }
        update闲谈,聊天s(for: content.title)
    }
    private static function update闲谈,聊天s(for contact: 字符串) {
        guard let 闲谈,聊天 = 闲谈,聊天模型.loaded闲谈,聊天s.first(where: { (闲谈,聊天) -> Bool in
            闲谈,聊天.contact == contact
        }) else {
            return assertionFailure("接收到的消息应该总是存在")
        }
        BaseNavigation视图Controller.navigationController?.viewControllers.forEach({ (viewController) in
            switch viewController {
            情况下让闲谈,聊天s视图Controller作为Updated闲谈,聊天Delegate:
                闲谈,聊天s视图Controller.updated(闲谈,聊天: 闲谈,聊天)
            default:
                打破
            }
        })
    }
    private static func 闲谈,聊天视图Controller(h和ling contact: 字符串) -> 闲谈,聊天视图Controller? {
        guard let 最后的视图Controller =
            BaseNavigation视图Controller.navigationController?.viewControllers.最后的
                as? 闲谈,聊天视图Controller,
            最后的视图Controller.闲谈,聊天.contact == contact else {
                return nil
        }
        return 最后的视图Controller
    }
}

我们必须手动挖掘视图控制器堆栈来弄清楚是否有一个视图控制器在我们得到推送通知后需要更新自己. 在本例中,我们还希望更新实现的屏幕 Updated闲谈,聊天Delegate, which, in this 情况下, is only the 闲谈,聊天s视图Controller. 我们这样做也是为了知道是否应该抑制通知因为我们已经在看 闲谈,聊天 it was meant for. 在这种情况下,我们最终会把消息传递给视图控制器. It’s pretty clear that PushNotificationController 需要对应用程序了解太多,才能完成它的工作.

如果 闲谈,聊天WebSocket 还会向应用程序的其他部分传递消息吗, 而不是一对一的关系 闲谈,聊天视图Controller,我们也会面临同样的问题.

很明显,每次添加另一个外部源时,我们都必须编写相当侵入性的代码. This code is also quite brittle, 因为它在很大程度上依赖于应用程序结构,并委托将数据传递回层次结构来工作.

Delegates

一旦我们添加了其他视图控制器,MVC模式也会增加额外的复杂性. 这是因为视图控制器倾向于通过委托了解彼此, initializers,—in the 情况下 of storyboards—prepareForSegue when passing data 和 references. 每个视图控制器处理自己到模型或中介控制器的连接, 它们都在发送和接收更新.

此外,视图通过委托与视图控制器通信. While this does work, 这意味着我们需要采取很多步骤来传递数据, 我总是发现自己围绕回调重构了很多,并检查委托是否真的设置好了.

通过更改另一个视图控制器中的代码来破坏一个视图控制器是可能的,比如 闲谈,聊天sList视图Controller because the 闲谈,聊天视图Controller is not calling updated(闲谈,聊天: 闲谈,聊天) anymore. 特别是在更复杂的场景中,保持所有内容同步是一件痛苦的事情.

Separation between 视图 和 模型

通过从视图控制器中移除所有与视图相关的代码 custom视图S并将所有与模型相关的代码移动到专门的控制器中, 视图控制器非常精简和分离. However, 还有一个问题:视图想要显示的内容与模型中的数据之间存在差距. A good example is the 闲谈,聊天List视图. 我们想要显示的是一个单元格列表,它告诉我们正在与谁交谈, what the 最后的 message was, 中还剩下多少未读邮件 闲谈,聊天:

聊天屏幕中的未读消息计数器.

然而,我们正在传递一个不知道我们想要看到什么的模型. Instead, it’s just a 闲谈,聊天 with a contact, containing messages:

class 闲谈,聊天 {
    let contact: 字符串
    var messages: [Message]
    init(with contact: 字符串, messages: [Message] = []) {
        自我.contact = contact
        自我.messages = messages
    }
}

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

    var unreadMessages: Int {
        return messages.filter {
            switch ($0.sender, $0.状态) {
                情况下 (.user, _), (.other, .read): return false
                情况下 (.other, .sending), (.other, .sent): return true
            }
        }.数
    }
    var 最后的Message: 日期? {
        return messages.最后的?.send日期
    }	

So finally we format the date in the 闲谈,聊天ItemTable视图Cell when we display it:

    func configure(with 闲谈,聊天: 闲谈,聊天) {
        participant.text = 闲谈,聊天.contact
        最后的Message.text = 闲谈,聊天.messages.最后的?.message ?? ""
        最后的Message日期.text = 闲谈,聊天.最后的Message.map { 最后的Message日期 in
            日期Renderer.string(from: 最后的Message日期)
        } ?? ""
        show(unreadMessageCount: 闲谈,聊天.unreadMessages)
    }

Even in a fairly simple example, 很明显,视图需要什么和模型提供什么之间存在紧张关系.

Static 事件-driven MVVM, a.k.a. 静态事件驱动的“视图模型模式”

Static MVVM works with view models, 但不是通过它们创建双向流量——就像我们过去通过mvc的视图控制器那样——我们创建了不可变的视图模型,每次UI需要更改以响应事件时都会更新UI.

事件几乎可以由代码的任何部分触发, 只要它能够提供事件所需的相关数据 枚举. For example, receiving the received(new: Message) 事件可以通过推送通知、WebSocket或常规网络调用触发.

Let’s see it in a diagram:

MVVM implementation flowchart.

At first glance, 它似乎比经典的MVC示例要复杂一些, 因为有更多的类涉及到完成完全相同的事情. 但仔细观察就会发现,没有一种关系是双向的.

更重要的是,每次UI更新都是由事件触发的, 所有发生的事情只有一条通过应用的路径. 你马上就能知道会发生什么. 如果需要,还可以清楚地看到应该在哪里添加一个新的, 或者在响应现有事件时添加新行为.

重构之后,我得到了很多新类,如上所示. 你可以找到我实现的静态MVVM版本 on GitHub. 然而,当我将这些变化与 cloc 工具,很明显,实际上没有那么多额外的代码:

Pattern文件空白CommentCode
MVC303862171807
MVVM514423591981

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

Lines-of-code pie charts. 视图 controllers: MVC 287 vs MVVM 154 or 47% less; 视图s: MVC 523 vs MVVM 392 or 26% less.

Also crucially, 最大的下降可以在MVC中通常最大的文件中找到:视图和视图控制器. 视图只有原来大小的74%视图控制器也只有原来大小的53%.

还应该注意的是,许多额外的代码是库代码,它们有助于将块附加到可视化树中的按钮和其他对象上, without requiring MVC’s classic @IBAction or delegate patterns.

让我们逐一探索这个设计的不同层次.

事件

The event is always an 枚举, usually with associated values. 它们通常会与模型中的一个实体重叠,但不一定如此. 在这种情况下,应用程序被分成两个主要事件 枚举s: 闲谈,聊天事件Message事件. 闲谈,聊天事件 是用于聊天对象本身的所有更新:

枚举 闲谈,聊天事件 {
    情况下 started
    情况下 loaded(闲谈,聊天s: [闲谈,聊天])
    情况下 creating(闲谈,聊天: 闲谈,聊天)
    情况下 created(闲谈,聊天: 闲谈,聊天)
    情况下 create闲谈,聊天Failed(原因:字符串)
}

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

枚举 Message事件 {
    案例发送(message: message, contact: 字符串, previousMessages: [message])
    情况下 sent(message: message, contact: 字符串)
    情况下 failedSending(message: message, contact: 字符串, reason: 字符串)
    情况下 received(message: message, contact: 字符串)
    情况下 userReads(messagesSentBy: 字符串)
    情况下 userRead(othersMessages: [Message], sentBy: 字符串)
    情况下 otherRead(yourMessage: Message, reader: 字符串)
}

It’s important to limit your *事件 枚举年代 a reasonable size. 如果你需要10个或更多的案例,这通常表明你试图涵盖多个主题.

Note: The 枚举 概念在斯威夫特中非常强大. I tend to use 枚举s with associated values a lot, 因为它们可以消除许多歧义,否则您将与可选值.

斯威夫特 MVVM Tutorial: 事件 Router

事件路由器是应用程序中发生的每个事件的入口点. 任何可以提供相关值的类都可以创建事件并将其发送到事件路由器. 所以它们可以被任何来源触发,e.g.:

  • 用户segue到一个特定的视图控制器
  • The user tapping a certain button
  • The application starting
  • External events like:
    • 返回失败或新数据的网络请求
    • Push notifications
    • WebSocket messages

事件路由器应该尽可能少地了解事件的来源,最好什么都不知道. 这个示例应用程序中的所有事件都没有指示它们来自何处, 所以很容易混合任何类型的消息源. 例如,WebSocket触发相同的事件-received(message: message, contact: 字符串)—as a new push notification.

事件(您已经猜到了)被路由到需要进一步处理这些事件的类. Usually, 唯一被调用的类是模型层(如果需要添加数据), changed, or removed) 和 the event h和ler. I’ll discuss both a bit more ahead, 但是事件路由器的主要特性是为所有事件提供一个简单的访问点,并将工作转发给其他类. Here’s the 闲谈,聊天事件Router as an example:

class 闲谈,聊天事件Router {
    静态函数路由(事件:闲谈,聊天事件) {
        switch event {
        情况下 .loaded(let 闲谈,聊天s):
            闲谈,聊天事件H和ler.loaded(闲谈,聊天s: 闲谈,聊天s)
        情况下 .creating闲谈,聊天(let contact):
            let 闲谈,聊天 = 闲谈,聊天模型.create(闲谈,聊天With: contact)
            闲谈,聊天Endpoint.create(闲谈,聊天: 闲谈,聊天)
            闲谈,聊天事件H和ler.creating闲谈,聊天()
        情况下 .created(let 闲谈,聊天):
            闲谈,聊天事件H和ler.created(闲谈,聊天: 闲谈,聊天)
        情况下 .create闲谈,聊天Failed(let reason):
            闲谈,聊天事件H和ler.failedCreating闲谈,聊天(reason: reason)
        }
    }
}

这里要做的事情很少:我们唯一要做的就是更新模型并将事件转发到 闲谈,聊天事件H和ler so the UI gets updated.

斯威夫特 MVVM教程:模型控制器

这与我们在MVC中使用的类完全相同,因为它已经工作得很好了. 它表示应用程序的状态,通常由Core Data或本地存储库支持.

模型层——如果在mvc中正确实现的话——很少需要重构来适应不同的模式. 最大的变化是可以通过更少的类来更改模型, 让变化发生的地方更清晰一些.

在另一种模式中, 您可以观察对模型的更改,并确保它们得到处理. 在这种情况下,我选择简单地只让 *事件Router*Endpoint 类更改模型,因此有一个明确的责任来决定何时何地更新模型. In contrast, if we were observing changes, 我们将不得不编写额外的代码来传播非模型更改事件(如错误) 闲谈,聊天事件H和ler这将使事件如何流经应用程序变得不那么明显.

斯威夫特 MVVM Tutorial: 事件 H和ler

事件处理程序是视图或视图控制器可以将自己注册(和注销)为侦听器以接收更新的视图模型的地方, which are built whenever the 闲谈,聊天事件Router calls a function on the 闲谈,聊天事件H和ler.

你可以看到它大致反映了我们之前在MVC中使用的所有视图状态. 如果你想要其他类型的UI更新(游戏邦注:如声音或触发Taptic引擎),你也可以在这里完成.

protocol 闲谈,聊天ListListening: class {
    函数更新(列表: 闲谈,聊天List视图模型)
}

Create闲谈,聊天Listening:类
    函数更新(创建:Create闲谈,聊天视图模型)
}

class 闲谈,聊天事件H和ler {
    private static var 闲谈,聊天ListListening?] = []
    private static var create闲谈,聊天Listening?] = []

    类函数添加(列表ener: 闲谈,聊天ListListening) {
        弱 var 弱Listener = 列表ener
        闲谈,聊天ListListening.append(弱Listener)
    }

    类函数remove(列表ener: 闲谈,聊天ListListening) {
        闲谈,聊天ListListening = 闲谈,聊天ListListening.filter { $0 !== 列表ener }
    }

    类函数添加(列表ener: Create闲谈,聊天Listening) {
        弱 var 弱Listener = 列表ener
        create闲谈,聊天Listening.append(弱Listener)
        列表ener.更新(创建:Create闲谈,聊天视图模型Builder.build(isSending: false, error: nil))
    }

    类函数移除(监听器:Create闲谈,聊天Listening) {
        create闲谈,聊天Listening = create闲谈,聊天Listening.filter { $0 !== 列表ener }
    }

    class func started() {
        闲谈,聊天Endpoint.fetch闲谈,聊天s()
        让loading视图模型 = 闲谈,聊天List视图模型Builder.buildLoading()
        闲谈,聊天ListListening.forEach { $0?.updated(列表: loading视图模型) }
    }

    class func loaded(闲谈,聊天s: [闲谈,聊天]) {
        let 闲谈,聊天List = 闲谈,聊天List视图模型Builder.build(for: 闲谈,聊天s)
        闲谈,聊天ListListening.forEach { $0?.updated(列表: 闲谈,聊天List) }
    }

    class func creating闲谈,聊天() {
        让create闲谈,聊天 = Create闲谈,聊天视图模型Builder.build(isSending: true, error: nil)
        create闲谈,聊天Listening.forEach { $0?.updated(create: create闲谈,聊天) }
    }

    类函数failedCreating闲谈,聊天(原因:字符串){
        让create闲谈,聊天 = Create闲谈,聊天视图模型Builder.build(isSending: false, error: reason)
        create闲谈,聊天Listening.forEach { $0?.updated(create: create闲谈,聊天) }
    }

    class func created(闲谈,聊天: 闲谈,聊天) {
        让create闲谈,聊天 = Create闲谈,聊天视图模型Builder.build(isSending: false, error: nil)
        create闲谈,聊天Listening.forEach { $0?.updated(create: create闲谈,聊天) }
        updateAll闲谈,聊天Lists()

        let 闲谈,聊天视图Controller = 闲谈,聊天视图Controller (for: 闲谈,聊天)
        BaseNavigation视图Controller.push视图Controller (闲谈,聊天视图Controller
                                                        animated: true,
                                                        removePreviousFromStack: true)
    }

    class func updateAll闲谈,聊天Lists() {
        让闲谈,聊天List视图模型 = 闲谈,聊天List视图模型Builder.build(for: 闲谈,聊天模型.all闲谈,聊天s())
        闲谈,聊天ListListening.forEach { $0?.updated(列表: 闲谈,聊天List视图模型) }
    }
}

这个类除了确保在某个事件发生时,正确的侦听器可以获得正确的视图模型之外,什么也不做. 如果需要设置初始状态,添加新的侦听器后可以立即获得视图模型. Always ensure you add a 引用列表以防止保留周期.

斯威夫特 MVVM Tutorial: 视图 模型

这是许多MVVM模式所做的与静态变体所做的最大区别之一. In this 情况下, 视图模型是不可变的,而不是将自己设置为模型和视图之间的永久双向绑定的中间. 为什么 would we do that? Let’s pause to explain it a moment.

要创建一个在所有可能情况下都能正常工作的应用程序,最重要的方面之一是确保应用程序的状态正确. 如果UI与模型不匹配或有过时的数据, 我们所做的一切都可能导致错误的数据被保存或应用程序崩溃或以意想不到的方式运行.

应用此模式的目标之一是,除非绝对必要,否则应用程序中没有状态. What is 状态, exactly? 状态基本上是我们存储特定类型数据表示的每个位置. 一种特殊类型的状态是UI当前所处的状态, 当然,我们无法用ui驱动的应用程序来阻止这种情况的发生. 其他类型的状态都与数据相关. If we have a copy of an array of 闲谈,聊天s backing up our UITable视图 在闲谈,聊天 List屏幕中,这是一个重复状态的例子. 传统的双向绑定视图模型是我们用户视图模型的另一个副本 闲谈,聊天s.

通过传递一个在每次模型更改时刷新的不可变视图模型, 我们消除了这种类型的重复状态, 因为在它应用到UI之后, it’s no longer used. 这样我们就只有两种无法避免的状态了——ui和model,而且它们彼此完全同步.

这里的视图模型与一些MVVM应用非常不同. 它仅作为所有标志的不可变数据存储, values, 块和视图需要的其他值来反映模型的状态, 但它不能被视图以任何方式更新.

因此它可以是一个简单的不可变 struct. To keep this struct 尽可能简单,我们将使用视图模型构建器实例化它. 视图模型的一个有趣之处在于它会获得行为标志,比如 shouldShowBusyshouldShowError that replace the 状态 枚举 先前在视图中发现的机制. Here’s the data for the 闲谈,聊天ItemTable视图Cell we had analyzed before:

struct 闲谈,聊天ListItem视图模型 {
    let contact: 字符串
    let message: 字符串
    let 最后的Message日期: 字符串
    let unreadMessageCount: Int
    let itemTapped: () -> Void
}

因为视图模型构建器已经处理了视图所需的确切值和操作, all data is preformatted. 此外,新的是一个块,将触发一旦项目被点击. 让我们看看视图模型构建器是如何生成它的.

视图 模型 Builder

视图模型构建器可以构建视图模型的实例,将输入转换为 闲谈,聊天年代或 MessageS转换为针对特定视图完美定制的视图模型. 视图模型构建器中发生的最重要的事情之一是确定视图模型中的块内部实际发生了什么. 视图模型构建器附加的块应该非常短, 尽快调用架构其他部分的函数. 这样的块不应该有任何业务逻辑.

类闲谈,聊天列表titemviewmodelbuilder {

    class func build(for 闲谈,聊天: 闲谈,聊天) -> 闲谈,聊天ListItem视图模型 {
        let 最后的MessageText = 闲谈,聊天.messages.最后的?.message ?? ""
        let 最后的Message日期 = (闲谈,聊天.messages.最后的?.send日期).map { 日期Renderer.string(from: $0) } ?? ""
        let unreadMessageCount = 闲谈,聊天模型.unreadMessages(for: 闲谈,聊天.contact).数

        返回闲谈,聊天列表titemviewmodel(联系人:聊天.contact,
                                     message: 最后的MessageText,
                                     最后的Message日期: 最后的Message日期,
                                     unreadMessageCount: unreadMessageCount,
                                     itemTapped: { show(闲谈,聊天: 闲谈,聊天) })
    }

    private类函数show(闲谈,聊天: 闲谈,聊天) {
        let 闲谈,聊天视图Controller = 闲谈,聊天视图Controller (for: 闲谈,聊天)
        BaseNavigation视图Controller.push视图Controller (闲谈,聊天视图Controller animated: true)
    }
}

现在所有的预格式化都发生在同一个地方,行为也在这里决定. 它是这个层次结构中相当重要的一个类,看看演示应用程序中的不同构建器是如何实现的,如何处理更复杂的场景会很有趣.

斯威夫特 MVVM Tutorial: 视图 Controller

这个架构中的视图控制器做的很少. 它会建立和拆除一切与它的观点有关的东西. 这样做是最合适的,因为它获得在适当的时间添加和删除侦听器所需的所有生命周期回调.

有时它需要更新根视图没有覆盖的UI元素, 比如标题或者导航栏的按钮. That’s why I usually still register the view controller as a 列表ener to the event router if I have a view model that covers the whole view for the given view controller; I forward the view model to the view afterward. But it’s also fine to register any UI视图 如果屏幕的某个部分具有不同的更新速率,则直接作为侦听器.g. 在某一特定公司的页面顶部放置一个实时股票行情.

The code for the 闲谈,聊天s视图Controller 现在已经短到不到一页了吗. 剩下的就是重写基视图, 在导航栏上添加和删除添加按钮, setting the title, adding it自我 as a 列表ener, implementing the 闲谈,聊天ListListening protocol:

类闲谈,聊天s视图Controller: UI视图Controller {
    private lazy var custom视图: 闲谈,聊天s视图 = {
        let custom视图 = 闲谈,聊天s视图()
        return custom视图
    }()
    private var addButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .添加、
        target: nil, action: nil)
    override func load视图() {
        view = custom视图
    }
    override func viewDidLoad() {
        超级.viewDidLoad()
        闲谈,聊天事件H和ler.add(列表ener: 自我)
        闲谈,聊天事件Router.route(event: .started)
        title = "闲谈,聊天s"
    }
    覆盖函数viewWillAppear(_动画:Bool) {
        超级.viewWillAppear(animated)
        navigationItem.rightBarButtonItem = addButton
    }
    覆盖函数viewWillDisappear(_ animated: Bool) {
        超级.viewWillDisappear(animated)
        navigationItem.rightBarButtonItem = nil
    }
}
扩展闲谈,聊天s视图Controller: 闲谈,聊天ListListening {
    函数更新(列表: 闲谈,聊天List视图模型) {
        addButton.action(block: { _ in
            列表.add闲谈,聊天()
        })
        custom视图.display(view模型: 列表)
    }
}

没有其他地方可以做的事情了,比如 闲谈,聊天s视图Controller is stripped to its bare minimum.

斯威夫特 MVVM Tutorial: 视图

不可变MVVM架构中的视图仍然可能相当沉重, as it still has a 列表 of tasks, 但我设法剥离了以下责任相比MVC架构:

  • 确定响应新状态需要更改的内容
  • 为操作实现委托和功能
  • 处理视图到视图的触发器,如手势和触发动画
  • 以一种可以显示的方式转换数据(如 日期年代 字符串s)

尤其是最后一点有相当大的优势. 在MVC, 当视图或视图控制器负责转换显示的数据时, 它总是在主线程上做这件事,因为很难将需要在主线程上发生的UI的真正变化与不需要在主线程上运行的事情分开. 在主线程上运行非ui更改代码会导致应用程序的响应速度变慢.

Instead, with this MVVM pattern, 从点击触发的block直到视图模型构建完成并传递给监听器——我们可以在一个单独的线程中运行这一切,最后只在主线程中进行UI更新. 如果我们的应用程序在主线程上花费更少的时间,它将运行得更流畅.

一旦视图模型将新状态应用于视图, 它被允许蒸发,而不是作为另一层状态存在. 所有可能触发事件的东西都被附加到视图中的一个项目上,我们不会与视图模型进行通信.

有一件事需要记住:您不必强制通过视图控制器将视图模型映射到视图. As mentioned before, 视图的某些部分可以由其他视图模型管理, especially when update rates vary. 考虑由不同的人编辑谷歌Sheet,同时为协作者保持一个打开的聊天窗格—每当有聊天消息到达时刷新文档并不是很有用.

一个众所周知的例子是键入查找实现,当我们输入更多文本时,搜索框会更新为更准确的结果. 这就是我如何实现自动完成 CreateAutocomplete视图 类:整个屏幕是由 Create视图模型 但是文本框正在听 AutocompleteContact视图模型 instead.

另一个例子是使用表单验证器, 可以构建为“局部循环”(附加或删除错误状态字段和声明表单是有效的)或通过触发事件完成.

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

通过使用静态MVVM实现,我们最终成功地完全分离了所有的层,因为视图模型现在在模型和视图之间架起了桥梁. 我们还简化了非用户行为引起的事件管理,并消除了应用程序不同部分之间的大量依赖关系. 视图控制器所做的唯一一件事就是将自己注册(和注销)到事件处理程序中,作为它想要接收的事件的监听器.

Benefits:

  • 视图和视图控制器实现往往要轻得多
  • 类更加专门化和分离
  • 事件可以很容易地从任何地方触发
  • 事件在系统中遵循可预测的路径
  • State is only updated from one place
  • 应用程序可以更高效,因为它更容易在主线程之外完成工作
  • 视图接收定制的视图模型,并与模型完全分离

Downsides:

  • 每次UI需要更新时,都会创建并发送一个完整的视图模型, 通常用相同的按钮文本覆盖相同的按钮文本, 用功能完全相同的积木来替换积木
  • 需要一些辅助扩展来使按钮点击和其他UI事件与视图模型中的块一起工作得很好
  • 事件 枚举在复杂的场景中,S可以很容易地增长到相当大,并且可能很难分割

最棒的是,这是一个纯斯威夫特模式:它不需要第三方斯威夫特 MVVM框架, 它也不排除使用经典MVC, 因此,您现在可以轻松地添加新功能或重构应用程序中有问题的部分,而不必重写整个应用程序.

还有其他方法可以对抗大型视图控制器,它们也提供了更好的分离. 我无法将它们全部详细地包括进来进行比较, 但让我们来简单看看一些替代方案:

  • Some form of the MVVM pattern
  • 某种形式的响应(使用Rx斯威夫特,有时与MVVM结合使用)
  • 模型-视图-表示器模式(MVP)
  • 视图-交互器-呈现器-实体-路由器模式(VIPER)

传统的MVVM用视图模型取代了大部分视图控制器代码,视图模型只是一个普通的类,可以更容易地在隔离中进行测试. 因为它需要成为视图和模型之间的双向桥梁,所以它经常实现某种形式的可观察对象. 这就是为什么你经常看到它与Rx斯威夫特这样的框架一起使用.

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

响应式编程风格最近越来越受欢迎,实际上它非常接近于带有事件的静态MVVM方法, as explained in this article. 主要的区别在于它通常需要一个框架, 你的很多代码都是专门针对这个框架的.

MVP是一种模式,其中视图控制器和视图都被认为是视图层. 呈现者转换模型并将其传递给视图层, 而我首先将数据转换为视图模型. 由于视图可以抽象为协议,因此测试起来要容易得多.

VIPER takes the presenter from MVP, 为业务逻辑添加单独的“交互器”, calls the model layer “entity,,并有一个路由器用于导航目的(并完成首字母缩略词)。. 它可以被认为是MVP的一种更详细和解耦的形式.


至此,静态事件驱动的MVVM得到了解释. 我期待在下面的评论中听到你的声音!

Underst和ing the basics

  • What is the use of MVVM?

    视图模型是一个独立且易于测试的类,它从视图控制器接管所有的逻辑和模型到视图的代码(通常还包括视图到模型的绑定).

  • What are protocols in iOS?

    协议(在其他语言中通常称为“接口”)是一组函数和变量,可以由任何类或结构实现. 因为协议没有绑定到特定的类, 可以使用任何类作为协议引用,只要它实现了它. This makes it much more flexible.

  • iOS中的委托模式是什么?

    委托是对基于协议的另一个类的弱引用. 委托通常用于在完成任务后向另一个对象“报告”, 不用把自己绑在一个特定的类上,也不用知道它的所有细节.

  • MVC和MVVM的区别是什么?

    在iOS中,MVVM不是MVC的替代品,它是一个补充. 视图控制器仍然发挥作用, 但是视图模型成为视图和模型之间的中介.

  • What is MVP in iOS?

    在iOS, MVP (model-view-presenter)模式是uiview和uiviewcontroller都是视图层的一部分. (Confusingly, 视图层是一个架构概念, 而UI视图来自UIKit通常也被称为视图.)

Hire a Toptal expert on this topic.
Hire Now
Lucas van Dongen's profile image
Lucas van Dongen

Located in Rotterdam, Netherl和s

Member since December 8, 2015

关于 the author

A mobile developer 和 teacher, Lucas不仅开发过iOS应用,还使用斯威夫特开发过后端api, Objective-C, Python, Elixir, C#.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Years of Experience

16

世界级的文章,每周发一次.

Subscription implies consent to our privacy policy

世界级的文章,每周发一次.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.