SwiftUI的整洁架构

Posted 编码890624

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SwiftUI的整洁架构相关的知识,希望对你有一定的参考价值。

原文:Alexey Naumov

翻译:小示 

校阅:小明890624

声明:本文翻译并非100%原文翻译,部分内容已转化为读者容易理解的语言进行翻译,如有不妥之处请在留言区留言。



SwiftUI的整洁架构

你能想象UIKit已经11岁了吗!自从2008年Apple发布了ios SDK, 我们就开始使用它来开发APP。在这段时间里,开发者们从来没有放弃寻找开发APP的完美架构。从最初的的MVC,到不断成长的MVP,MVVM,VIPER,RIBs和VIP。

但是最近发生了一些值得关注的事情,这使得大多数当前iOS开发中使用的架构模式将成为历史。这就是SwiftUI,iOS开发的未来。它将极大地影响iOS开发中的架构设计现状。

SwiftUI的整洁架构


SwiftUI的整洁架构

1

SwiftUI的整洁架构

本质上的变化


UIKit是一个命令式的、事件驱动的框架。我们可以引用层级结构中的每个视图,在加载视图或响应一个事件(点击按钮或一个新的数据可以显示在UITableView中)时更新视图。在处理这些事件时,使用Callback,Delegate,Target-Action等。
现在,SwiftUI的出现让这一切都成为过去。SwiftUI是一个声明式的、状态驱动的框架。我们不再需要引用任何层级结构中的视图,也不需要为了响应一个事件去直接修改视图。相反,我们修改与视图绑定的State。 Delegate,Target-Action,Responder Chain,KVO(※链接1) 已经完全被闭包和绑定替代。
SwiftUI里的所有View都是一个结构体,他们的创建速度比相似地UIView的子类的创建速度快很多倍。这种结构体维持着和状态的关系,并使用body来渲染UI。

SwiftUI的整洁架构


SwiftUI中的一个视图只是一个程式化的函数。开发者提供一个输入(状态),它做出对应的输出(显示)。唯一地可以改变显示结果的方式就是改变输入的状态:我们无法通过添加和移除子视图来改变body函数,换句话说:唯一可能对已经显示的视图进行的改变,必须定义在body中,并且无法在运行时中改变。

在SwiftUI中,开发者并不是添加或者移除子视图,而是用提前定义好的流程算法启用或者禁用UI块。


SwiftUI的整洁架构

2

SwiftUI的整洁架构

MVVM是新的标准架构


SwiftUI集成了MVVM。

举一个最简单的例子,一个视图不依赖任何外部状态,它内部的@State 变量扮演着ViewModel的角色,在状态变化时为刷新UI提供订阅机制(绑定)。在更复杂的场景中,视图能够引用一个外部的ObservableObject,也就是一个外部的ViewModel。(顺带说一下,如果你想要了解更多ObservableObject,@ObservedObject以及其他一些SwiftUI和Combine中优质的构造,作者建议你读他的另一篇文章:以SwiftUI的State为基础的新东西 )

不管怎样,SwiftUI中视图与State的工作方式和经典的MVVM非常相似(除非我们介绍一个更复杂的程序的架构)。


SwiftUI的整洁架构

你不再需要ViewController。

让我们看一个简单的MVVM模型的SwiftUI APP的例子:

Model:数据容器

struct Country { let name: String}


View:一个SwiftUI的视图

struct CountriesList: View {
@ObservedObject var viewModel: ViewModel var body: some View { List(viewModel.countries) { country in Text(country.name) } .onAppear { self.viewModel.loadCountries() } }}


ViewModel:一个ObservableObject,包含业务逻辑,并允许视图观察状态的改变

extension CountriesList { class ViewModel: ObservableObject { @Published private(set) var countries: [Country] = []
private let service: WebService
func loadCountries() { service.getCountries { [weak self] result in self?.countries = result.value ?? [] } } }}


在这个简化的例子中,当视图出现在屏幕中,onAppear调用ViewModel的loadCountries(),以此触发网络请求并加载WebService的数据。ViewModel通过callback接收数据,并将数据传递给被View监控的@Published变量countries。


SwiftUI的整洁架构



尽管这篇文章是关于整洁架构的,作者还是为 原项目 (※链接2)建立了一个 MVVM版本的分支 (※链接3 )。如果感兴趣,可以比较一下并选出更适合自己的那个。项目的关键特性主要有:
  • 普通的 SwiftUI + Combine 实践

  • 分解 表示层,业务逻辑层和数据层

  • 全测试覆盖,包括UI(感谢ViewInspector ※链接4)

  • 使用类Redux 的中心化 AppState作为唯一的总数据源

  • 程序化导航(支持深层链接)

  • 基于泛型的简单而灵活的网络层

  • 处理系统事件 (当应用处于非活动状态时,模糊视图层次结构)


SwiftUI的整洁架构

3

SwiftUI的整洁架构

掀开引擎盖,SwiftUI是基于ELM的


从28分26秒看这个视频,这兄弟2017年就有了SwiftUI的工作原型!
让我们感兴趣的是是否我们可以使用ELM的其他概念来改善我们的SwiftUI APP。
看了ELM语言的网站上关于 ELM (※链接5) 架构的描述后,我并没有发现什么新的东西。SwiftUI和ELM有相同的本质:
  • Model — APP的状态

  • View —将状态转变成html的方式

  • Update — 基于消息更新状态的方式

我们好像在哪见过,你记得吗?

SwiftUI的整洁架构

我们已经有了Model,视图是从Model自动生成的,我们唯一能改进的是更新传递的方式。我们可以采用REDUX的方式,并使用Command模式来更改状态,而不是在SwiftUI的视图和其他模块中直接写入状态。尽管我在之前UIKit的项目(ReSwift ❤)中喜欢用REDUX的方法,但问题是对于SwiftUI APP来说这是否必要—数据流已经处于控制中并且很容易追踪。


SwiftUI的整洁架构

4

SwiftUI的整洁架构

协调器(Coordinator)已经成为历史


协调器(又称 Router)是VIPER、RIBs和MVVM-R架构中必不可少的一部分。在UIKit应用中合理的分配了单独模块用于屏幕导航—从一个ViewController到另一个的直接路径导致他们紧密耦合,更不用说一个ViewController里层级复杂的代码联系了。

不像UIKit中的视图,我们不能让一个SwiftUI的视图调整子视图的布局,或者把它渲染到图像上下文。如果没有渲染引擎,SwiftUI的视图将会完全没用。渲染引擎拥有状态(State),而且视图只是在渲染时接受一个对于状态(State)的参考,即使它只是用一个视图内部的@State(※链接6)。

SwiftUI的视图只是一个绘制算法。这就是为什么从SwiftUI的视图中提取路径那么困难:路径是这个算法不可或缺的一部分。

我们应该顺应这个特性,对程序进行结构设计,以便该绘制算法的主要部分分布在单独的视图里。同时,将业务逻辑提取到易于隔离测试的纯结构模块中。

不像UIKit中的视图,我们不能让一个SwiftUI的视图调整子视图的布局,或者把它渲染到图像上下文。如果没有渲染引擎,SwiftUI的视图毫无用处。渲染引擎拥有状态(State),而且视图只是在渲染时接受一个对于状态(State)的参考,即使它只是用一个视图内部的@State。

SwiftUI的视图只是一个绘制算法。这就是为什么从SwiftUI的视图中提取路径那么困难:路径是这个算法不可或缺的一部分。

我们应该顺应这个特性,对程序进行结构设计,以便该绘制算法的主要部分分布在单独的视图里。同时,将业务逻辑提取到易于隔离测试的纯结构模块中。

现在好了,SwiftUI使协调器不再重要。更改视图层级结构的每个视图(即NavigationView、TabView或.sheet())都适用绑定(Binding)来控制显示内容。

绑定是状态变量的外部形式—你可以读写它,但是它实际上从属于另一个模块。

当用户在TabView中选择了一个Tab,你无法获得回调。而是TabView通过绑定改变了值:“displayedTab = .userFavorites”。

开发者随时可以为绑定赋值,与此同时,TabView也会做出相应的变化。

在SwiftUI中程序导航完全被状态通过绑定控制。作者写了一篇文章(※链接7)来讨论这个问题。


SwiftUI的整洁架构

5

SwiftUI的整洁架构

在SwiftUI中可以用VIPER、RIBs和VIP吗?


尽管我么可以从这些架构中借鉴很多很棒的想法和概念,但最终,对于SwiftUI的APP来说他们中任何一个的规范的实践都没有意义。

首先,我们都已经知道了,我们不需要Router。

其次,SwiftUI全新设计的数据流,和对视图状态绑定的原生支持,将所需的配置代码缩到了Presenter成为无用实体的程度。

随着模式中模型数量的减少,我们也不再需要Builder。因此,从根本上来说,由于整个模式所要解决的问题不存在了,模式本身也变得多余。

SwiftUI在系统设计中为自己引入了一系列的挑战,因此必须重新设计UIKit的模式。很多人无论如何都想使用受欢迎的架构,但是没必要。※链接8


SwiftUI的整洁架构

6

SwiftUI的整洁架构

整洁架构


参考VIP先驱鲍勃大叔的整洁架构(※链接9)。

通过把软件分成不同的层,同时遵从依赖规则,你将创造一个完全可测试的、有所有已经提到过的优点的系统。

整洁架构对引入的层数的要求非常宽松,因为这取决于应用程序本身。 但是对于移动APP中最普通的情况来说,开发者只要三个层:
  1. 表示层
  2. 业务逻辑层
  3. 数据层

因此,如果我们根据SwiftUI提炼出整洁架构的特性,我会想到以下内容:


SwiftUI的整洁架构


我做了一个demo(※链接10)来展示这种架构的使用。这个APP从restcountries.eu(※链接11)获取国家列表和详情并显示。


SwiftUI的整洁架构

7

SwiftUI的整洁架构

AppState


AppState是架构中唯一需要成为对象的实体(即ObservableObject)。(※链接12)或者,它可以是一个Combine库里CurrentValueSubject的封装。

就像是REDUX,AppState作为单一真值来源并保存整个APP里所有的状态,包括用户数据、秘钥、导航状态(选中的Tab、弹出的sheet)和系统状态(是否已激活、是否在后台等等)。

AppState不知道任何其它层的信息,也不包含任何业务逻辑。

Demo中的一个简单的 AppState (※链接13)的例子:
class AppState: ObservableObject, Equatable { @Published var userData = UserData() @Published var routing = ViewRouting() @Published var system = System()}


SwiftUI的整洁架构

8

SwiftUI的整洁架构

View


一个普通的SwiftUI视图可能没有状态或者只有内部的状态(State)变量。

其他任何层都不知道视图层的存在,因此,也不需要隐藏协议。

当View实例化的时候,它会通过SwiftUI的@Environment、@EnvironmentObject或者@ObservedObject来接受AppState和Interactor。

作为用户操作(比如按下按钮)或者视图生命周期中onAppear的响应,并传递给Interactor。

struct CountriesList: View {
@EnvironmentObject var appState: AppState @Environment(\.interactors) var interactors: InteractorsContainer
var body: some View { ... .onAppear { self.interactors.countriesInteractor.loadCountries() } }}


SwiftUI的整洁架构

9

SwiftUI的整洁架构

Interactor


Interactor包含了特定View或一组View的业务逻辑。和AppState共同组成了业务逻辑层,使其完全独立于展示和外部资源。

它是无状态的,而且只引用通过构造函数注入的AppState对象。

通常Interactor都遵守一个协议,以便在测试中使用一个模拟Interactor。

Interactor接收到请求后开始工作(例如:从网络获取数据或者进行计算),但是它们从不直接返回数据(包括使用闭包)。

相反,他们通过AppState或者绑定将结果传递给视图。

使用绑定的情况是:当返回结果不属于AppState并只被一个视图使用时,也就是说,返回结果不需要被持久保存或者不在其他视图中被使用。

下面是demo里的CountriesInteractor(※链接14):

protocol CountriesInteractor { func loadCountries() func load(countryDetails: Binding<Loadable<Country.Details>>, country: Country)}
// MARK: - Implemetation
struct RealCountriesInteractor: CountriesInteractor {
let webRepository: CountriesWebRepository let appState: AppState
init(webRepository: CountriesWebRepository, appState: AppState) { self.webRepository = webRepository self.appState = appState }
func loadCountries() { appState.userData.countries = .isLoading(last: appState.userData.countries.value) weak var weakAppState = appState _ = webRepository.loadCountries() .sinkToLoadable { weakAppState?.userData.countries = $0 } }
func load(countryDetails: Binding<Loadable<Country.Details>>, country: Country) { countryDetails.wrappedValue = .isLoading(last: countryDetails.wrappedValue.value) _ = webRepository.loadCountryDetails(country: country) .sinkToLoadable { countryDetails.wrappedValue = $0 } }}


SwiftUI的整洁架构

10

SwiftUI的整洁架构

Repository


Repository是一个读取数据的抽象的网关,提供从数据服务(webServer或本地数据库)中存取数据的功能。

我有一篇文章(※链接15)专门讨论了为什么抽象出Repository是必要的。

举例,如果APP要使用自己的后端、谷歌地图API并往本地数据库写入数据,需要有三个Repository:两个用来负责不同的网络服务,一个用于操作本地数据库。

Repository也是无状态的,也不向AppState写入数据,它只包含操作数据的逻辑。并且,它完全不清楚视图和Interactor。

具体的Repository应该被隐藏在protocol后,以便在测试的时候Interactor可以和模拟的Repository交互。

下面是Demo中的CountriesWebRepository(※链接16):

protocol CountriesWebRepository: WebRepository { func loadCountries() -> AnyPublisher<[Country], Error> func loadCountryDetails(country: Country) -> AnyPublisher<Country.Details.Intermediate, Error>}
// MARK: - Implemetation
struct RealCountriesWebRepository: CountriesWebRepository { let session: URLSession let baseURL: String let bgQueue = DispatchQueue(label: "bg_parse_queue") init(session: URLSession, baseURL: String) { self.session = session self.baseURL = baseURL } func loadCountries() -> AnyPublisher<[Country], Error> { return call(endpoint: API.allCountries) }
func loadCountryDetails(country: Country) -> AnyPublisher<Country.Details, Error> { return call(endpoint: API.countryDetails(country)) }}
// MARK: - API
extension RealCountriesWebRepository { enum API: APICall { case allCountries case countryDetails(Country) var path: String { ... } var httpMethod: String { ... } var headers: [String: String]? { ... } }}

由于WebRepository把URLSession作为实例变量,在测试中很容易通过自定义URLProtocol来模拟网络请求(※链接17)。


最后

现在demo的测试覆盖了97%的代码,全都是因为整洁架构的依赖规则和APP中各个层的相互隔离。


SwiftUI的整洁架构



文中外部链接

※链接1:https://nalexn.github.io/callbacks-part-1-delegation-notificationcenter-kvo/※链接2:https://github.com/nalexn/clean-architecture-swiftui※链接3:https://github.com/nalexn/clean-architecture-swiftui/tree/mvvm※链接4:https://github.com/nalexn/ViewInspector※链接5:https://guide.elm-lang.org/architecture/※链接6:https://nalexn.github.io/stranger-things-swiftui-state/※链接7:http://nalexn.github.io/swiftui-deep-linking/※链接8:https://theswiftdev.com/2019/09/18/how-to-build-swiftui-apps-using-viper/※链接9:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html※链接10:https://github.com/nalexn/clean-architecture-swiftui※链接11:https://restcountries.eu/※链接12:https://nalexn.github.io/swiftui-observableobject/※链接13:https://github.com/nalexn/clean-architecture-swiftui/blob/master/CountriesSwiftUI/Injected/AppState.swift※链接14:https://github.com/nalexn/clean-architecture-swiftui/blob/master/CountriesSwiftUI/Interactors/CountriesInteractor.swift※链接15:https://nalexn.github.io/separation-of-concerns/※链接16:https://github.com/nalexn/clean-architecture-swiftui/blob/master/CountriesSwiftUI/Repositories/CountriesWebRepository.swift※链接17:https://github.com/nalexn/clean-architecture-swiftui/blob/master/UnitTests/NetworkMocking/RequestMocking.swift


SwiftUI的整洁架构

END


如果您喜欢这篇文章--请点击右下方【⚛在看】

如果您还没有关注--可以通过下方二维码添加关注

Wish U a nice day!




扫描二维码

获取更多精彩

编码890624



以上是关于SwiftUI的整洁架构的主要内容,如果未能解决你的问题,请参考以下文章

《架构整洁之道》

架构整洁之道 15~29章读书笔记

在 SwiftUI 中装饰文本片段的最佳方法是啥?

架构整洁之道 30~34章读书笔记

好书推荐探究构架设计的方法论 | 《架构整洁之道》

好书推荐探究构架设计的方法论 | 《架构整洁之道》