这篇文章最初是由作者在 更好的编程 on 媒介.

原文: SwiftUI:选择应用架构——如果你不知道自己需要什么,很难决定.


SwiftUI:选择应用架构

如果你不知道自己需要什么,很难决定.

SwiftUI的声明式软件开发方法让它更有优势, 为iOS编写现代应用程序要容易得多, Mac, 以及苹果生态系统的其他部分.

但SwiftUI也并非没有自身的挑战和问题, 其中一个问题就是我们应该在全新的SwiftUI应用程序中使用什么样的架构?

这是一个经常被问到的问题——也许没有人会感到惊讶——也是一个经常被回答的问题. 事实上,关于这个主题有很多很多的文章、书籍和演讲.

不幸的是, 其中相当一部分是由渴望把自己喜欢的架构带到SwiftUI的人完成的. 因此,我们面临着铺天盖地的文章,告诉我们为什么我们应该使用MVVM, 或反应/回家的, 或清洁, 或毒蛇, 或柠檬酸. 甚至没有.

我想在这方面我和其他人一样有罪, 考虑到我自己写的关于这个主题的文章.

然而,问题仍然是:我们应该选择哪一个?

我们如何决定?

我们用什么标准来做选择呢?

什么标准确实……?

你看,随着最后一个问题的提出,地平线上开始出现了一线曙光. 也许, 只是也许, 如果我们知道另一个问题的答案,就更容易回答我们的架构问题:

我们希望我们的架构解决什么问题?

定义问题

物理学家有句老话:时间是阻止一切同时发生的东西.

既然时间存在,既然一切 突然发生, 作为人类,我们决定创建一些度量单位,以便更好地组织事情.

比如分和秒. 不同的时间段,我们将其进一步聚合并划分为小时和天, 数月乃至数年, 等等. 公共单位,一个公共单位 框架 我们可以用它来讨论时间. 欠缺这. To 理解 it.

同样的,当我们 可以 作为软件开发人员,我们选择将我们的应用程序编写为一个单一的、巨大的代码块 要做到这一点. 而不是, 我们试图将我们的应用程序分解成更小的更容易理解的单元. 我们尝试编写具有明确定义的角色、行为和职责的类、结构和函数. 这是一个视图. 这就是模型. 这里的代码是一个提供模型的服务. 等等.

应用程序架构基本上归结为我们用来决定的规则 如何我们将代码分解为所有这些独立的组件. 为什么这一部分会消失 在那里 这部分为什么 在这里.

多年来,我们制定了一些指导方针来帮助我们. 想法就像 单一职责原则, 原则, 依赖性倒置原则, 关注点分离.

如果这还不够,我们甚至还添加了一些高级概念,比如 单一的真相来源, 函数式编程 单向数据流.

其中一些想法配合得很好. 其他的可能会增加额外的复杂性,甚至使一个简单的程序更难编写, 理解, 和维护. 有些只是善意的尝试,强加在其他语言、Swift和iOS等平台上的限制和问题.

应用程序体系结构——任何应用程序体系结构——都试图将所有这些规则和指导原则编纂并平衡成一组我们可以使用和遵循的最佳实践.

如果我们的努力成功了, 如果我们选择了我们的组件和架构, 最后得到一个函数, 优雅的, 易于理解的软件.

So. 如果我们要为SwiftUI定义一个理想的架构,那就是我们想从它那里得到什么?

性能和兼容性

如果有人问我这个问题, 我的首要标准之一是,它需要与SwiftUI本身很好地配合.

除了用一个简单的, 声明式接口, SwiftUI带来了另一个主要概念:国家, 一份数据只有一个真实来源的想法.

在2019年的苹果全球开发者大会上,一名男子站在台上展示名为《Single Source of Truth》的数据流程图.

WWDC ' 19“数据流通过SwiftUI”演示

更新该状态,程序就会自动显示更改, 在这个过程中重新配置和动画自身.

然而,有些人把单一的真相来源发挥到了极致. 对于给定的数据,是否应该有唯一的真实来源, 他们问, 那为什么不提供一个真相来源 整个应用程序?

这是基于React/Redux背后概念的架构的基础, 虽然这个概念一开始看起来很合理, 用在SwiftUI上也不是没有缺点.

我写了很长时间 SwiftUI的视图、状态和性能, 但最重要的是,SwiftUI可以非常高效地确定任何给定数据更改的后果, 它只会重绘受该更改影响的接口部分.

创建一个全局状态, 然而, 我们可能会在大型应用程序中遇到性能问题,当这种状态发生变化时,需要SwiftUI检查和/或重建整个视图树中的每个依赖项(i.e. 整个应用程序),每次都是 任何 改变发生.

另一种选择,正如在苹果的 数据流通过SwiftUI表示,是将状态绑定到视图层次结构中尽可能低的位置.

在2019年苹果全球开发者大会上,一名男子在台上展示名为@EnvironmentObject的数据流程图.

WWDC19“数据流通过SwiftUI”演示

当我们在层次结构的下层绑定时, 我们极大地减少了接口更新和渲染所需的数量,因为只有视图树的一小部分会受到任何给定状态变化的影响.

大型全局状态的另一个缺点是,如果您将该状态导入到单个视图中,那么所有内容都将公开给所有人看. 像这样, 在不遍历视图中的每一行代码的情况下,您如何确定一个给定的视图可能正在访问或操作哪些信息?

不太好.

你可以编写额外的代码来过滤状态,使其适合于所讨论的视图……但这样做基本上是为了解决你自己创建的问题而编写更多的代码.

这不是最理想的情况.

我强烈建议你阅读 SwiftUI的视图、状态和性能 如果你对SwiftUI背后发生的事情感兴趣,请阅读本文, 但与此同时,我认为我们可以利用上述问题来排除那些类型的架构.

下一个是什么?

简单

SwiftUI提供了一种简单的声明式方法来编写应用程序. 也许更重要的是,这是 简洁的. 您可以用很少的代码完成很多工作.

像这样, 用过于正式的体系结构来负担我们自己和我们的应用程序将是一种耻辱,这将再次戏剧性地增加我们需要编写的代码量.

尤其是样板代码. 这可能只是个人喜好,但我 讨厌样板代码. 进一步, 我还认为,更多的代码往往会导致更多的bug, 代码越多,小傻瓜就有越多的藏身之处. 因为样板代码是, 好吧, 样板, 为了避免再次输入所有内容,它往往会被大量复制和粘贴,这反过来会导致代码中出现一些偷偷摸摸的复制/粘贴错误.

这, 对我来说, 倾向于排除像VIPER这样过于正式的架构,因为它坚持将应用程序的每个部分都分解为视图, 扶少团团员, 主持人, 实体, 和路由器. VIPER架构很大程度上基于 单一职责原则 但在我看来,它倾向于将这种想法发挥到极致.

VIPER传统上是与基于uikit的应用程序配对的,在这种应用程序中,我们有一个不幸的倾向,即为应用程序中的每个屏幕创建一个单一的大型UIViewController. 制作不同的视图和xib以及嵌套视图控制器通常是非常繁琐的工作, 所以很多时候我们都懒得去做.

因此,我们寻找一种方法,将尽可能多的逻辑和用户交互移出视图控制器……我们做到了, 每一个都变成了谜题中独立的小部分.

VIPER创造了很多小的移动部件, 因此,为管理VIPER而编写的代码中有很大一部分是针对这些代码编写的, 嗯……管理毒蛇.

尽管如此,我们还是尽可能地遵循 单一职责原则 有可取之处. 幸运的是,SwiftUI支持我们。

视图组成

如果有一件事在全球开发者大会的SwiftUI会议上反复出现, SwiftUI视图是非常轻量级的,在创建它们的过程中几乎没有性能损失.

因此,在SwiftUI中,您可以根据应用程序的需要创建尽可能多的独特和特殊用途的视图.

我写了很多关于这个概念的文章 SwiftUI作曲的最佳实践,在 在SwiftUI中查看构图所以,如果你想知道更多,我建议你把这些文章添加到你的阅读清单.

但这里的关键是,如果我们用较小的视图和视图组件构建应用程序, 那么我们就不需要像VIPER这样的解决方案通常产生的额外复杂性.

测试

这个钟摆可能摆得太大, 然而, 根据目前说过的那些我们不需要的东西来决定 任何 体系结构的. 把我们所有的代码都塞到一些小视图中就行了.

考虑以下视图.

OrderDetailsRowView:视图{
    var项目:OrderItem
    var主体:一些视图{
        HStack {
            如果项目.数量== 1 {
                文本(项.名称)
            其他}{
                文本(“\(项目.名称)(\美元(项目.量,说明符:“%.2 f”)@ $ \(项目.价格,说明符:“%.2f")")
            }
            垫片()
            文本(“$(\(项目.总说明符:“%.2f")")
        }
    }
}

这是SwiftUI中小型专用视图的一个很好的例子. 好吧, 伟大的, 也许, 除了所有的逻辑和格式都挤在视图体中之外.

这使得我们很难编写确保视图输出正确的测试用例.

关注点分离

通常被提出的主要体系结构是MVVM(模型-视图-视图模型).

也就是说, 它应该写成模型-视图模型-视图(MVMV), 因为视图模型存在于应用程序的数据(模型)和视图的需求(布局)之间。.

使用视图模型, 我们想把尽可能多的逻辑移出视图, 留下了非常容易理解的代码.

这是一个很好的例子 关注点分离. 我们将业务逻辑放在围栏的一边, 所有视图展示和布局都在另一边.

用一个例子来说明可能会更好, 让我们考虑下面的SwiftUI视图,它是前面那个的父视图 OrderDetailsRowView 观点:

OrderDetailsView:视图{ 

     @StateObject var vm = OrderDetailsViewModel() 

     var主体:一些视图{

     {形式
        如果虚拟机.消息.hasMessage {
            StatusMessageView(类型:虚拟机.消息)
        }
        LabelValueRowView(标签:Order,值:vm).dateValue)
          
        ForEach (vm.项目){项目在
            OrderDetailsRowView(项目:项目)
        }
        如果虚拟机.hasDiscount {
            LabelValueRowView(标签:Subtotal,值:vm).小计)
            OrderDetailsDiscountView(价值:vm.折扣)
        }
        LabelValueRowView(标签:vml.totalLabel、价值:vm.总)

        按钮(“订单”){
            自我.vm.重新排序()
        }
     }
     .onAppear {
        vm.load ()
     }
   }
}

请注意,e的推崇 在这个视图中是由视图模型驱动的.

所有的条件值和计算值都来自视图模型. 有几个 if 语句控制某些元素的可见性,但同样,这些元素背后的逻辑 决定 是模型做的吗. 视图只是实现了它们.

当状态改变时, 点击“再次订购”按钮, 视图再次基于视图模型重新生成

如果我们测试视图模型 if 我们看到了所需的输出 if 我们的视图控制器被正确地绑定到视图模型, 然后我们就可以相当有把握地说,我们的屏幕和代码是正确的.

测试专用的观点

虽然我个人认为MVVM非常适合SwiftUI,但它甚至可能 it 在某些情况下是过度的吗. 并不是每个视图都需要一个不同的视图模型.

例如,我们可以将原始的细节行视图重构为如下所示.

OrderDetailsRowView:视图{

    var项目:OrderItem var主体:一些视图{
        HStack {
            文本(项Description)
            垫片()
            文本(项Total)
        }
    }

    var 项Description:字符串{
        如果项目.数量== 1 {
            返回项目.的名字
        其他}{
            返回“\(项目.名称)(\(项目.formattedQuantity) @ \(项目.formattedPrice))”
        }
    }

    var 项Total: String {
        项.formattedTotal
    }
}

有了这个视图主体, 我们显示的是两部分数据,这很容易理解, 从业务逻辑的角度来看,这里不会有太多可能出错的地方.

有了条件逻辑和格式分解成不同的变量,就可以实例化视图 本身 单品和 测试 我们的逻辑,看看它是否正确,然后用两个项目制作另一个,并测试它.

func 测试OrderDetailsRowView () {

    让view1 = OrderDetailsRowView(项目:OrderItem.mock1)
    XCTAssert (view1.项Description == "软饮料")
    XCTAssert (view1.项Total = = " $ 1.99")

    让view2 = OrderDetailsRowView(项目:OrderItem.mock2)
    XCTAssert (view2.项Description == "芝士汉堡(2 @ $4.99)")
    XCTAssert (view2.项Total = = " $ 9.98")}

同样,我们只是需要尽可能多地移动逻辑 视图体,并将其放入我们可以看到的变量和函数中 视图.

因为,套用一句军事格言:如果我们能看到它,我们就能测试它.

也就是说, 如果一个给定的视图开始变得太大,我会开始考虑如何把它分成更小的视图,或者我将开始移动我的条件代码和格式到一个专用的视图模型.

如果一个给定的视图需要处理API请求,则处理错误和错误消息, 像空列表一样管理边缘情况, 等等, 那么我肯定会转向一个专用的视图模型.

有关使用网络请求逻辑正确设置VM的更多信息,请参见: 使用SwiftUI中的视图模型协议? 你做错了.

SwiftUI架构标准

综上所述,我选择SwiftUI架构的标准如下…

  1. 它必须是性能,而不管应用程序的大小.
  2. 它必须兼容SwiftUI的行为和状态管理.
  3. 它应该是简洁的、轻量级的、适应性强的和灵活的.
  4. 它鼓励SwiftUI查看构图.
  5. 它支持测试.

换句话说,它让SwiftUI成为SwiftUI.

完成块

似乎是我牵着你的手,带领你走上了通往MVVM圣坛的道路, 公平地说, 我就是这么做的). 但至少我们现在知道了 为什么 我们就在它面前,基于我们所做的选择.

你同意我的标准吗? 你自己也有一些? 同意我的结论还是我遗漏了什么? 无论哪种方式,我都想知道,所以请在下面的评论区给我留言.

想要更多有趣的故事? 请随意查看我的 SwiftUI系列.

迈克尔长阿凡达

我写苹果、斯威夫特和科技. 我是CRi 解决方案的首席iOS工程师, 领先的移动企业和金融应用程序.