SwiftUI的整洁架构
Posted 编码890624
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SwiftUI的整洁架构相关的知识,希望对你有一定的参考价值。
原文:Alexey Naumov
翻译:小示
校阅:小明890624
声明:本文翻译并非100%原文翻译,部分内容已转化为读者容易理解的语言进行翻译,如有不妥之处请在留言区留言。
整
洁
架
构
你能想象UIKit已经11岁了吗!自从2008年Apple发布了ios SDK, 我们就开始使用它来开发APP。在这段时间里,开发者们从来没有放弃寻找开发APP的完美架构。从最初的的MVC,到不断成长的MVP,MVVM,VIPER,RIBs和VIP。
但是最近发生了一些值得关注的事情,这使得大多数当前iOS开发中使用的架构模式将成为历史。这就是SwiftUI,iOS开发的未来。它将极大地影响iOS开发中的架构设计现状。
1
本质上的变化
SwiftUI中的一个视图只是一个程式化的函数。开发者提供一个输入(状态),它做出对应的输出(显示)。唯一地可以改变显示结果的方式就是改变输入的状态:我们无法通过添加和移除子视图来改变body函数,换句话说:唯一可能对已经显示的视图进行的改变,必须定义在body中,并且无法在运行时中改变。
在SwiftUI中,开发者并不是添加或者移除子视图,而是用提前定义好的流程算法启用或者禁用UI块。
2
MVVM是新的标准架构
SwiftUI集成了MVVM。
举一个最简单的例子,一个视图不依赖任何外部状态,它内部的@State 变量扮演着ViewModel的角色,在状态变化时为刷新UI提供订阅机制(绑定)。在更复杂的场景中,视图能够引用一个外部的ObservableObject,也就是一个外部的ViewModel。(顺带说一下,如果你想要了解更多ObservableObject,@ObservedObject以及其他一些SwiftUI和Combine中优质的构造,作者建议你读他的另一篇文章:以SwiftUI的State为基础的新东西 )
不管怎样,SwiftUI中视图与State的工作方式和经典的MVVM非常相似(除非我们介绍一个更复杂的程序的架构)。
你不再需要ViewController。
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 + Combine 实践
分解 表示层,业务逻辑层和数据层
全测试覆盖,包括UI(感谢ViewInspector ※链接4)
使用类Redux 的中心化 AppState作为唯一的总数据源
程序化导航(支持深层链接)
基于泛型的简单而灵活的网络层
处理系统事件 (当应用处于非活动状态时,模糊视图层次结构)
3
掀开引擎盖,SwiftUI是基于ELM的
Model — APP的状态
View —将状态转变成html的方式
Update — 基于消息更新状态的方式
我们已经有了Model,视图是从Model自动生成的,我们唯一能改进的是更新传递的方式。我们可以采用REDUX的方式,并使用Command模式来更改状态,而不是在SwiftUI的视图和其他模块中直接写入状态。尽管我在之前UIKit的项目(ReSwift ❤)中喜欢用REDUX的方法,但问题是对于SwiftUI APP来说这是否必要—数据流已经处于控制中并且很容易追踪。
4
协调器(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)来讨论这个问题。
5
在SwiftUI中可以用VIPER、RIBs和VIP吗?
尽管我么可以从这些架构中借鉴很多很棒的想法和概念,但最终,对于SwiftUI的APP来说他们中任何一个的规范的实践都没有意义。
首先,我们都已经知道了,我们不需要Router。
其次,SwiftUI全新设计的数据流,和对视图状态绑定的原生支持,将所需的配置代码缩到了Presenter成为无用实体的程度。
随着模式中模型数量的减少,我们也不再需要Builder。因此,从根本上来说,由于整个模式所要解决的问题不存在了,模式本身也变得多余。
SwiftUI在系统设计中为自己引入了一系列的挑战,因此必须重新设计UIKit的模式。很多人无论如何都想使用受欢迎的架构,但是没必要。※链接8
6
整洁架构
参考VIP先驱鲍勃大叔的整洁架构(※链接9)。
通过把软件分成不同的层,同时遵从依赖规则,你将创造一个完全可测试的、有所有已经提到过的优点的系统。
-
表示层 -
业务逻辑层 -
数据层
因此,如果我们根据SwiftUI提炼出整洁架构的特性,我会想到以下内容:
我做了一个demo(※链接10)来展示这种架构的使用。这个APP从restcountries.eu(※链接11)获取国家列表和详情并显示。
7
AppState
AppState是架构中唯一需要成为对象的实体(即ObservableObject)。(※链接12)或者,它可以是一个Combine库里CurrentValueSubject的封装。
就像是REDUX,AppState作为单一真值来源并保存整个APP里所有的状态,包括用户数据、秘钥、导航状态(选中的Tab、弹出的sheet)和系统状态(是否已激活、是否在后台等等)。
AppState不知道任何其它层的信息,也不包含任何业务逻辑。
class AppState: ObservableObject, Equatable {
var userData = UserData()
var routing = ViewRouting()
var system = System()
}
8
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()
}
}
}
9
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 }
}
}
10
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中各个层的相互隔离。
文中外部链接
※链接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
END
如果您喜欢这篇文章--请点击右下方【⚛在看】
如果您还没有关注--可以通过下方二维码添加关注
Wish U a nice day!
扫描二维码
获取更多精彩
编码890624
以上是关于SwiftUI的整洁架构的主要内容,如果未能解决你的问题,请参考以下文章