- 原文地址: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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。