在iOS上使用UIKit的轻量级MVI架构

961 阅读7分钟

[

Patrick Ngo

](medium.com/@patngo?sou…)

帕特里克-恩戈

关注

6月25日

-

6分钟阅读

[

保存

](medium.com/m/signin?ac…)

在iOS上使用UIKit的轻量级MVI架构

背景介绍

在我担任iOS工程师的过程中,我几乎接触过所有类型的架构模式,从vanilla MVC一直到VIPER以及中间的所有MV。正如我们大多数人随着时间的推移意识到的那样,在选择这种模式时,没有正确或错误的答案,它只是一个随着时间的推移适应项目和团队需求的问题。

话虽如此,在与React Native进行了相当多的合作,也对Flutter和SwiftUI进行了修补,很明显,整个前端行业正在朝着这些可以说是更 "现代 "的单向数据流声明性UI的概念发展。

尽管UIKit很快就会被淘汰,但我还是想尝试最后一种模式,看看我们是否能在一个真正的生产应用中加入这些概念的一些影子。这就是我遇到MVI模式的原因。这里有一篇关于它的伟大文章,我从中得到了很大的启发。然而,我想试着把它简化一下。所以这是我自己在iOS上使用UIKit的MVI模式的味道。

需要考虑的问题

  • 必须使用UIKit(因为我们仍然以iOS 11用户为目标🥲
  • 适用于中等规模的团队(类似于MVVM的用例)。
  • 最少的模板(如VIPER或RIB)。
  • 容易上手(需要对RxSwift或反应式概念有最少的了解)
  • 单向的数据流和(几乎)基于状态的声明式UI
  • 可扩展性(它应该能够适应不断增长的团队和产品需求)。

什么是MVI?

MVI就是Model View Intent。虽然它被称为MVI,但更贴切的名字是状态视图意图。它与其他常见模式的关键区别之一是它非常严格地使用单向数据流。

  • 状态(模型)
  • 视图
  • 意图

MVI图示

状态

  • 虽然相似,但不应与传统MV架构中的Model相混淆
  • 它是一个不可改变的数据结构,代表了应用程序这部分在任何时候的状态,就像一个快照一样。
  • 包含业务逻辑的结果(即来自服务请求的响应),以及代表UI的状态(加载、文本字段值等)

视图

  • 在概念上,它与其他常见的MV架构相同
  • 它可以是一个UIView或一个UIViewController
  • 与MV最大的不同是,它有一个暴露的*update(with state: State)*函数。这是一个纯粹的函数,接收新的状态和之前的状态作为输入。
  • 考虑到相同的输入,View的输出应该总是相同的。
    因此,View可以被认为是状态的函数f(State)=View的产物。

意图

  • 在概念上,它类似于MVVM中的ViewModel或RIB/Viper中的Interactor。
  • 它可以访问所有需要的服务和其他依赖关系
  • 它处理来自视图的所有用户交互
  • 它处理所有的业务逻辑
  • 它处理所有的状态变化
  • 与MVVM/RIB/Viper的一个很大的区别是,新的状态是通过一个单一的状态流发射到视图中的。
  • 因此,数据的流动是单向的

例子

让我们考虑一个有2个输入和一个保存按钮的表单屏幕的小例子,在这个例子中是用来输入一个地址。

屏幕示例

状态

状态示例

AddressState是一个有3个属性的简单结构。它符合State协议,需要一个静态的initialState,这将在后面发挥作用。它还有一个特殊的init,可以从之前的状态中生成一个新的状态,并带有所需的修改值。

视图

在这种情况下,我们的视图是一个简单的UIViewController。它符合View的要求,因此有一个*update(with state)*函数。这个函数负责根据所有的状态值来更新UI。

注意到我们是如何将状态与之前的状态进行比较,并在更新用户界面之前确保它们是不同的。在这种情况下,这似乎是微不足道的,但你可以想象更复杂的情况,我们要避免任何不必要的UI更新。视图还负责接收用户的输入并将其发送到Intent,在这个例子中,它负责更新输入字段以及点击保存按钮。

请注意,视图中几乎没有任何反应式代码,例如订阅*可观察变量。*它只是简单地接收状态(从哪里接收并不重要)并更新UI。

意向

AddressIntent与你所期望的ViewModelInteractor没有太大区别。它处理一些输入并发出状态变化。请注意,除了stateDriver的绑定,这里也有最小的反应式代码。这就是主要的神奇之处。你可以把它看作是与React中的 *setState()*非常相似。

我们可以看到,stateDriver只是一个包装好的BehaviorRelay,它将通过一直发出新的状态和之前的状态来驱动它所绑定的视图。请注意,我们在这里使用了RxSwift来实现StateDriver,但它也可以很容易地用Combine或任何其他反应式概念来实现。

就这样了。我们现在有了一个完全工作的MVI。对于一个有3个输入的简单的表单屏幕来说,这看起来是不是有很多的代码?也许吧。然而,对于有许多状态的复杂用例,我发现它实际上简化了相当多的代码量。

导航

有一个问题可能会立即浮现在脑海中,那就是如何处理导航。像大多数其他MVx模式一样,MVI并没有规定一个关于导航的具体模式。在我们的特定项目中,我们只是决定采用协调员模式。所以从技术上讲,我们可以称之为MVI+C

渐进式采用

你也可以想象,如果你正在处理一个使用MVVM+C或类似的现有项目,那么这个架构可以被逐步采用,因为只有协调员内部的内容可以从MVVM改为MVI。同样地,如果你正在处理一个VIPER代码库,Interactor+Entity可以被Intent+State所取代。

可扩展性

反过来说,随着代码库的增长,我们可以看到这种模式可能开始出现裂缝。然而,它可以根据需要进行演变。如前所述,我们可以采用协调员路由器模式来处理导航。我们可以引入一个Builder模式来处理依赖关系的注入。如果我们发现Intent在将业务数据转换为视图数据方面做得太多,我们可以考虑引入一个Presenter模式。

测试

这种模式特别适合于快照测试。每个视图都可以用所有的状态对象组合进行测试,以生成该视图的所有可能的状态。

缺点

UIKit的命令性有时对这种模式不利。

  • TableView / CollectionView。UICollectionView/UITableView APIs(委托)的异步设计使其难以在不 "暂时 "保留状态的情况下进行更新。
  • 文本输入。 文本值应该由传入的状态设置,而不是直接由用户输入。这需要对UITextFieldUITextView委托的工作方式进行一些操作。
  • 高效的更新。通过与之前的状态相比较,避免不必要的UI更新。

总结

总的来说,我对这个结果相当满意。我们在中型/大型的应用程序中使用了这种模式,它为我们提供了相当好的服务。它确实满足了我们之前提到的所有的考虑。它也使UI编码变得更加有趣。它确实需要一点思维方式的转变,特别是对于那些非常习惯于命令式方法,而从未处理过声明式UI的人来说,比如React/Flutter/SwiftUI/Compose/等等。然而,一旦它被 "击中",它就会一帆风顺。😎