- 原文地址:UIKit or SwiftUI: Which Should You Use in Production?
- 原文做者:Alexey Naumov
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:苏苏的 霜羽 Hoarfroster
- 校对者:Zilin Zhu、lsvih
Apple 最近发布了 iOS 14 —— 这意味着 Apple 已经给了开发者们一年时间缓冲决定是否使用 SwiftUI 了。并且这意味着 SwiftUI 不只能够被爱好者们在其闲余的项目中使用,并且要被企业团队评估是否能在其发布的应用中使用了。前端
从每一个人的评论的字面上看,你们都说编写 SwiftUI 代码颇有趣,可是 SwiftUI 到底是仅仅只能充当一个玩具喂饱开发者们的好奇心,仍是真的能够看成一个专业的工具在实际生产中使用呢?而若是咱们须要在实际生产中使用 SwiftUI,那咱们就须要以对待一个专业工具而非对待玩物的态度,开始着手考虑它的稳定性和灵活性了。android
那咱们应该在何时开始在实际生产的应用的代码中使用 SwiftUI?ios
若是您准备在 2020 年至 2022 年之间开始一个新的重大项目,这个问题着实很难回答。git
SwiftUI 带来了很多创新,可是即便在 iOS 14 上运行使用 SwiftUI 构建的应用,咱们仍然会遇到错误,而且 SwiftUI 的自定义灵活性不足 。程序员
尽管能够适时地使用 UIKit 来缓解这些问题,但您可否估计有多少代码最终是用 UIKit 写的呢?从长远来看,SwiftUI 有没有可能成为负担,使您以为不如单纯使用 UIKit 呢?github
真要解决这个问题,咱们只能打赌到了 iOS 15 的发布先后,SwiftUI 的问题都被解决了。这意味着最快须要到 2022 年(iOS 16 发布的时候),咱们才能彻底信任 SwiftUI。数据库
在本文中,我将详细介绍如何在以下两种状况下搭建项目:swift
从过往的经验上看,UI 框架对于移动应用程序的结构体系有着很是大的影响 —— 在 iOS 开发上,咱们使用 UIKit 并围绕它构建了几乎全部其余的代码。后端
回忆一下您上一个使用了 UIKit 的项目,并尝试评估一下,若是要彻底摆脱 UIKit,将其替换为另外一个 UI 框架(例如 AsyncDisplayKit 须要耗费多少精力?markdown
—— 对于大多数项目,这可能意味着要彻底重写代码。
网络开发者们会嘲笑咱们,毕竟他们老是在各类 UI 框架中切换着使用。所以,他们早已定下了应用与依赖库之间的原则,并将 UI 仅仅看成程序的其中一小部分,就像具体使用哪一种数据库同样,可有可无。
这是否意味着咱们(移动开发人员)将 UI 与业务逻辑层捆绑的太死了?但是咱们应该作到了啊 —— MVC、MVVM、VIPER 等框架都在帮助着咱们。
但咱们仍然受困于 UI 库啊。
移动应用不多负责任何核心的业务逻辑 —— 例如计算贷款利息并批准贷款。每一个企业但愿最大程度地减小各类风险,所以他们会让应用程序上传数据并在后端完成计算。
可是,现代移动应用程序上仍然有不少业务逻辑,可是这种计算与上面提到的那些计算是不一样的 —— 它只专一于 UI 的呈现,而不是业务运行的核心逻辑。
这意味着咱们须要作得更好 —— 将与 UI 呈现相关的计算与正在使用的 UI 框架分离。
若是咱们不这样作,也难怪框架会彻底捆绑到代码库中。
UIKit 和 SwiftUI 的 API 都粘在了别的非表示层代码中 —— 这些框架正在迫使开发人员将他们变成应用的核心,推进将表示层与其余全部的东西紧密联系,甚至是在根本不是 UI 的地方也要与 UI 捆绑使用!
以 SwiftUI 中的 @FetchRequest
为例。它在表示层中捆绑了 CoreData
模型。看起来的确非常方便。但这同时严重违背了 CS 中的多种软件设计原则和最佳作法 —— 这样的代码能够在短时间内节省时间,但从长远来看可能会对项目形成极大的危害。
@AppStorage
怎么样?数据、文件操做就在表示层中实现。那您又该如何测试这些代码?您能够轻松识别容器中的键名的冲突吗?您可否能将其无缝迁移到其余数据存储类型,例如使用钥匙串存储数据?
当开发速度得以最大化的提升,咱们都忽略了质量、可维护性和代码重用性。
那不一样界面之间的导航又会如何表现呢?
UIKit 老是对咱们耳语:“噢!你快点用 presentViewController(:,animation:,completion :);
代码替换掉旧代码吧!不要再使用那些代码了!”
而 SwiftUI 不会低语 —— 它只会在向咱们大声嚷嚷:“听个人,除非你按照我想要的方式来作,要么我就会以精心设计的方式搞垮你的应用!”
有没有一种方法能够保护咱们的代码库免受这些野蛮的 API 的侵害?
固然是有的!这种大声嚷嚷的 API 一般状况下是挺好的 —— 让程序员更不容易犯错。可是,当此类 API 没法正常工做时,这就会变成一个巨大的问题,例如 SwiftUI 的自动化页面导航就出现了问题。
如您所见,框架中到处是陷阱,您使用框架越多的功能,你就越难在特定屏幕或整个应用程序中中止使用此框架。
若是咱们但愿应用足够的顽强,能够忍耐 UIKit 与 SwiftUI 之间的痛苦过渡(反之亦然),则须要确保表示层和其余层之间不是简简单单的一个木栅栏分割,而是一堵巨墙彻底割裂它们。
我指的是,什么都不该该被留下 —— 即便是字符串格式化也应该彻底扔出表示层。
您是否能够在没有 UIKit 或 SwiftUI 库的支持下将浮点数 5434.35
转换为 $5,434.35
?就让咱们在表示层以外完成这项工做!
UI 框架在屏幕之间的导航的 API 是否会让视图粘合其余代码?就让咱们把导航隔离开!
咱们不只须要从 UI 层中提取尽量多的逻辑,并且还须要使 UIKit 组件或 SwiftUI 组件与获取数据的函数彻底兼容。
咱们如何让 UIKit 和 SwiftUI 之间兼容?
咱们知道,SwiftUI 是彻底由数据驱动的 —— 须要提供响应式数据的绑定。幸运的是,UIKit 能够与 MVVM 和响应式框架一块儿变换。
这意味着数据源,委托,目标操做以及其余 UIKit API 应该在 UI 层中被隔离开。
—— import UIKit
不该出如今任何的 ViewModel 中。
我要提醒一下,只要 UI 组件是彻底由数据驱动的,则显示模块的确切架构模式并不重要。为了简化示例,我将在本文中说起 MVVM。
如今。咱们应该为 ViewModel 使用哪个响应式框架?咱们知道 SwiftUI 仅能够与 Combine 一块儿使用,而 UIKit 最好与 RxCocoa 一块儿使用。
—— 两种方法均可行,所以这取决于您是否支持 iOS 13 即 Combine 以及您对 RxSwift 的喜好程度。
让咱们同时考虑下这两个方法吧!
从 iOS 13 开始咱们就可使用 Combine 套件,对于仍然须要支持 iOS 11 或 12 的用户来讲这可不是个什么好消息。
在这里,我将讨论一种将 UIKit + RxSwift 迁移到 SwiftUI + RxSwift 的简便方法。
考虑一下下面给出的极简配置:
class HomeViewModel {
let isLoadingData: Driver<Bool>
let disposeBag = DisposeBag()
func doSomething() { ... }
}
class HomeViewController: UIViewController {
let loadingIndicator: UIActivityIndicatorView!
let viewModel = HomeViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel
.isLoadingData
.drive(loadingIndicator.rx.isAnimating)
.disposed(by: disposeBag)
}
@IBAction func handleButtonPressed() {
viewModel.doSomething()
}
}
复制代码
这个视图是彻底由数据驱动的 —— ViewModel 彻底控制视图的状态、内容更改。
那若是让咱们在不涉及 ViewModel 的代码的状况下将该界面迁移到 SwiftUI?
有两种方法能够尝试:
Driver
(或Observable
)的 @Published
的变量绑定一个新的 ObservableObject
。2.在 SwiftUI 的视图内,将每一个 Driver
适配为 Publisher
并绑定到 @State
。
Observable
到 @Published
的迁移对于第一种方法,咱们须要建立一个新的 ObservableObject
,以将原来的 ViewModel 中的每一个 Observable
变量镜像过去:
extension HomeViewModel {
class Adapter: ObservableObject {
let viewModel: HomeViewModel
@Published var isLoadingData = false
}
}
struct HomeView: View {
let adapter: HomeViewModel.Adapter
var body: some View {
if adapter.isLoadingData {
ProgressView()
}
Button("Do something!") {
self.adapter.viewModel.doSomething()
}
}
}
复制代码
原来的 ViewModel 和适配器之间的值的绑定代码应尽量简洁。这是在 Driver
和 Observable
的状况下桥接的样子:
let observable: Observable<Bool> = ...
observable
.bind(to: self.binder(\.isLoadingData))
.disposed(by: disposeBag)
let driver: Driver<Bool> = ...
driver
.drive(self.binder(\.name))
.disposed(by: disposeBag)
复制代码
这里咱们须要的是使用 RxSwift 的 Binder
,它会将值分配给特定的 @Published
值。这是进行桥接的 binder
函数的代码片断:
extension ObservableObject {
func binder<Value>(_ keyPath: WritableKeyPath<Self, Value>) -> Binder<Value> {
Binder(self) { (object, value) in
var _object = object
_object[keyPath: keyPath] = value
}
}
}
复制代码
回到咱们的 ViewModel,您能够在 Adapter
的初始化中进行绑定:
extension HomeViewModel {
class Adapter: ObservableObject {
let viewModel: HomeViewModel
private let disposeBag = DisposeBag()
@Published var isLoadingData = false
init(viewModel: HomeViewModel) {
self.viewModel = viewModel
viewModel.isLoadingData
.drive(self.binder(\.isLoadingData))
.disposed(by: self.disposeBag)
}
}
}
复制代码
这种方法的一个缺点是必须为您拥有的每一个 @Published
变量重复一份模版化的代码。
Observable
到 @State
第二种方法只须要设置较少的代码,而且基于 SwiftUI 变成可使用外部状态的另外一种方式:使用 View 的 onReceive
方法,将值分配给本地的 @State
。
这里的好处是咱们能够直接在 SwiftUI 视图中使用原始的 ViewModel:
struct HomeView: View {
let viewModel: HomeViewModel
@State var isLoadingData = false
var body: some View {
if isLoadingData {
ProgressView()
}
Button("Do something!") {
self.viewModel.doSomething()
}
.onReceive(viewModel.isLoadingData.publisher) {
self.isLoadingData = $0
}
}
}
复制代码
这里的 viewModel.isLoadingData
是一个 Driver
,也所以咱们须要将其转化为 Combine 中的 Publisher
。
开源社区中已经发布了 RxCombine 库,该库支持从 Observable
到 Publisher
的桥接,所以使用该库支持 Driver
会很简单:
import RxCombine
import RxCocoa
extension Driver {
var publisher: AnyPublisher<Element, Never> {
return self.asObservable()
.publisher
.catch { _ in Empty<Element, Never>() }
.eraseToAnyPublisher()
}
}
复制代码
若是您能够只支持 iOS 13+,则能够考虑在应用程序中使用 Combine 构建网络和其余非 UI 模块。
即便将 Combine 与 UIKit 绑定起来有些不便,但从长远来看,当项目彻底迁移到 SwiftUI 时,选择 Combine 做为驱动应用程序中数据的核心框架老是有所裨益的。
并且同时,您能够在 sink
函数中更新 UIKit 视图:
viewModel.$userName
.sink { [weak self] name in
self?.nameLabel.text = name
}
.store(in: &cancelBag)
复制代码
或者,您能够利用上述 RxCombine 库将 RxCocoa 中可用的数据绑定转换为 Publisher
或 Observable
。
viewModel.$userName // Publisher
.asObservable() // Observable
.bind(to: nameLabel.rx.text) // RxCocoa 绑定
.disposed(by: disposeBag)
复制代码
我应该注意,若是咱们在应用程序中选择 Combine 做为主要的响应框架,则 RxSwift、RxCocoa 和 RxCombine 的使用应仅限于将数据绑定到 UIKit 视图,这样咱们就能够轻松摆脱这些依赖关系以及应用程序中的最后一个 UIKit 视图。
在这种状况下,ViewModel 应该仅使用 Combine 来构建(不要再使用 import RxSwift
!)。
让咱们一块儿回到原始的示例:
class HomeViewModel: ObservableObject {
@Published var isLoadingData = false
func doSomething() { ... }
}
class HomeViewController: UIViewController {
let loadingIndicator: UIActivityIndicatorView!
let viewModel = HomeViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel
.$isLoadingData // 获取 Publisher
.asObservable() // 转换到 Observable
.bind(to: loadingIndicator.rx.isAnimating) // RxCocoa 绑定
.disposed(by: disposeBag)
}
@IBAction func handleButtonPressed() {
viewModel.doSomething()
}
}
复制代码
当须要在 SwiftUI 中重建此屏幕时,一切都将为您完成:在 ViewModel 中无需进行任何更改。
过去,我曾探讨 程序导航 在 SwiftUI 中的工做方式,根据个人经验,这是 SwiftUI 中仍然充满着苦难的部分。这里发生着各类故障和崩溃,而且不支持动画的自定义。
随着时间的流逝,这些问题确定会获得解决,可是到目前为止,我丝绝不信任 SwiftUI 的页面间导航功能。
中止使用 SwiftUI 的页面导航后,咱们其实并不会损失太多 —— 只要 SwiftUI 还是由 UIKit 支持的,与咱们使用 UIKit 所实现的性能相比,就不会有什么大的性能差别。
在为本文构建的示例项目中,我使用了传统的协调器模式(MVVM-R),该模式适用于使用 SwiftUI 中的 UIHostingController
构建的页面。
若是咱们想控制与使用特定 UI 框架有关的风险,咱们应该付出更多的努力来控制其在代码库中的扩展。
SwiftUI 存在的问题不该阻止您至少在可预见的未来准备将您的项目要迁移到此框架。
从 UI 层中提取尽量多的业务逻辑,并使 UIKit 屏幕由数据驱动。这样,迁移到 SwiftUI 变得垂手可得。
我用普通的登陆/主页/细节屏幕构建了一个 示例项目,该屏幕演示了 UIKit 和 SwiftUI 视图如何变得再也不重要,让你能够轻松分离并更换。
有两个目标 —— 一个在 UIKit 上运行,另外一个在 SwiftUI 上,其余都共享代码库的基本部分。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。