
收錄:原文地址
原作者:姜沂(傾寒)
前言
SwiftUI 是蘋果公司于 2019 年推出的 Apple Platform 的新一代聲明式布局引擎,筆者于去年第一時間升級 Beta 嘗鮮全家庭,并在短時間內(nèi)迅速落地了基于 SwiftUI 的內(nèi)部 APP, 也分享了幾篇關(guān)于 SwiftUI 的文章, 但 SwiftUI 1.0 基本沒有任何公司敢用在正式上線的主 APP 上,API 在 Beta 版本之間各種廢棄,UI 樣式經(jīng)常不兼容,大列表性能差,彼時都標(biāo)識著 SwiftUI 還稱為一個 Toy Framewrok.
隨著 WWDC 20 相關(guān)新特性和介紹視頻的釋出,都明確的宣告著 SwiftUI 元年已經(jīng)到了,SwiftUI 已經(jīng)成長為新時代的布局引擎。
以下從幾個方面分享關(guān)于 SwiftUI 的重大改變及核心優(yōu)勢。
PS: 需要讀者對 Swift 及 SwiftUI 1.0 有一定熟悉。
SwiftUI Apps
蘋果在最近幾年的動作中一直在搞 Apple Platform 統(tǒng)一的事情,從最近幾年的 iPad 多任務(wù) 多窗口,到 Mac Catalyst 再到今年更進(jìn)一步直接推出了 Apple silicon 芯片更是從硬件上做到了真正統(tǒng)一(話外音:你們在軟件上玩的那些跨平臺的都是小玩意,硬件才是王道)。
還提供了 Rosetta2 Universal2 幫助開發(fā)者基本無成本的遷移到新平臺上。但是作為軟件工程師還是要更多的關(guān)注軟件生態(tài)的變化。首先了解下創(chuàng)建 APP 時的變化
可以看到創(chuàng)建新工程時有了一套全新的模板基于 SwiftUI App Lifecycle 的跨平臺項目。
代碼也從原本的基于 UI/NS HostViewController 變成了基于 APP 的聲明式描述,下面是代碼的前后對比.
- Before
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
}
- After
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
其中@main 是Swift 5.1 新增的 Attribute 標(biāo)記了應(yīng)用程序的入口點(diǎn),更多請參看 SE-0281-main-attribute.md
乍看好像只有代碼精簡了不少,很多人會認(rèn)為這個簡潔程度還不如Flutter 的 main() => runApp(MyApp());.
但最重要的變化是這是第一次跨平臺代碼,完全無需引入任何 UIKit APPKit WatckKit 等相關(guān)Framewok, 即可直接運(yùn)行在不同平臺上。這意味著我們后續(xù)在UI布局系統(tǒng)上可以逐漸擺脫對傳統(tǒng)命令式 UI 編程的依賴。達(dá)到真正的平臺無關(guān)。
SwiftUI 將整個原有的平臺差異部分抽象為 App 和 Scene,對于一個 mac/iOS/iPad/watch/tv/..應(yīng)用,來說 App 代表了整個應(yīng)用,Scene 代表了與 Window 相關(guān)的多窗口,有些設(shè)備只有一個 Scene 有些則有多個,雖然不同的 OS 確實(shí)存在差異,但是在語義層面達(dá)到了一致。
其次一個沒有歷史包袱的 APP,也可以完整的從 Swift APP lifecycle 風(fēng)格式的模板開始,無需再和傳統(tǒng)的 UIKit/APPKit 等混合。這也意味著可以達(dá)到 APP 完全 Declared and State-Driven。
Viusal Editing
Preview
在傳統(tǒng)的利用 DSL 可視編程框架或者平臺,諸如 Web Flutter 等技術(shù),都是開發(fā)者編寫好對應(yīng)的代碼,運(yùn)行在對應(yīng)的平臺或者調(diào)試工具上。SwiftUI 作為蘋果最重要的軟件層戰(zhàn)略框架,更是和 Xcode 深度結(jié)合,在運(yùn)行之前就可以完整的預(yù)覽你所編寫的界面。
強(qiáng)大的 Preview 可以讓你既可以從編寫 DSL 到立即預(yù)覽效果,也可從預(yù)覽的 Canvas 畫布中直接修改效果在代碼編輯器中生成代碼,這對于日常開發(fā)的效率有非常大的提高,尤其是在 UI 微調(diào)時,效果尤為突出。
Xcode12 可以在 Canvas 上同時預(yù)覽多個不同設(shè)備環(huán)境的界面,也可以直接投射到真實(shí)的設(shè)備上來預(yù)覽。
對于日常開發(fā)來說,編寫一個UI界面通常依賴外部的網(wǎng)絡(luò)/磁盤/其他數(shù)據(jù),才能正常的構(gòu)建,這也造成了UI開發(fā)雖然是開發(fā)中較為簡單的一步,但同時也是最耗時的一步,有了預(yù)覽功能,可以把很多繁瑣的工作前置解決掉,對于研發(fā)效率會有非常大的提高。
Xcode Library
在編寫真實(shí)項目中,一個公司的 APP UI 包含成百上千種風(fēng)格的 View 組件,對于 UI 組件豐富的產(chǎn)品,如果一個新需求可以由現(xiàn)有的組件組合,那么需求交付的時間也會大大縮短。
但是對于一個大型的開發(fā)團(tuán)隊而言,一個開發(fā)同學(xué)是很難知道公司內(nèi)到底有多少種組件庫,而且即便知道有某種組件庫,開發(fā)同學(xué)初期看到的也是代碼,一般需要書寫一定的 Demo 才可以用眼睛感知到這個組件到底是否是我想要的。
在 Xcode 12 中提供了更強(qiáng)大的工具,一個自定義組件,只需要遵守一個 LiberyContentProvider 協(xié)議就可被Xcode識別,可以像系統(tǒng)控件一樣直接從 Xcode 里面識別并預(yù)覽。對于一個大型團(tuán)隊來說,此功能可以大大提高找尋組件和查看組件樣式的效率。
// Without trailing closure:
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}, completion: { _ in
self.view.removeFromSuperview()
})
// With trailing closure
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}) { _ in
self.view.removeFromSuperview()
}
// Multiple trailing closure arguments
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
} completion: { _ in
self.view.removeFromSuperview()
}
DSL
隨著 Swift5.3 和 SwiftUI2.0 的推出,SwiftUI 在 DSL 上也更富有表現(xiàn)力, Swift 支持了多重尾閉包語法和在 ViewBuilde 里面支持 Switch Case 語句。
Multiple Trailing Closures
雖然社區(qū)對多重尾閉包的討論上一直存在爭議問題,但最終 Swift5.3 還是接受并實(shí)現(xiàn)了,在普通命令式編程的地方使用會有一定的困惑性,但是在 SwiftUI 中 DSL 也更有聲明式的味道。
// Without trailing closure:
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}, completion: { _ in
self.view.removeFromSuperview()
})
// With trailing closure
UIView.animate(withDuration: 0.3, animations: {
self.view.alpha = 0
}) { _ in
self.view.removeFromSuperview()
}
// Multiple trailing closure arguments
UIView.animate(withDuration: 0.3) {
self.view.alpha = 0
} completion: { _ in
self.view.removeFromSuperview()
}
Switch Case Support
在 SwiftUI 的 ViewBuilder DSL體系中也支持了 Switch case 語法。
var body: some View {
switch c {
case .a:
return Text("A")
case .b:
return Text("B")
case .c:
return Text("C")
}
}
Data Flow
在使用傳統(tǒng)命令式編程編寫 UI 代碼時,開發(fā)者需要手動處理 UIView 和 數(shù)據(jù)之間的依賴關(guān)系,每當(dāng)一個 UIView 使用了外部的數(shù)據(jù)源,就表明了 UIView 對外部的數(shù)據(jù)產(chǎn)生了依賴,當(dāng)一個數(shù)據(jù)產(chǎn)生變化時,如果意外的沒有同步UIView的狀態(tài),那么 Bug 就產(chǎn)生了。
處理簡單的依賴關(guān)系是可控的,但是在真實(shí)項目中,視圖之間的依賴關(guān)系是非常復(fù)雜的,假設(shè)一個視圖只有 4 種狀態(tài),組合起來就有 16 種,再加上時序的不同,情況就更加復(fù)雜。
人腦處理狀態(tài)的復(fù)雜度是有限的,狀態(tài)的復(fù)雜度一旦超過人腦的復(fù)雜度,就會產(chǎn)生大量的 Bug,并且修掉了這個產(chǎn)生了新的Bug。
那么 SwiftUI 是如何解決這個問題的?
SwiftUI 的框架提供了幾個核心概念:
統(tǒng)一的 body 屬性,SwiftUI 自動從當(dāng)前 App 狀態(tài)集自動生成基于當(dāng)前狀態(tài)的快照 View。
統(tǒng)一的數(shù)據(jù)流動原語。
關(guān)于 SwiftUI 中的 Data Flow 是如何消除視圖和狀態(tài)不一致的,請參考去年撰寫的文檔 系列文章《深度解讀SwiftUI 背后那些事兒》
今年 SwiftUI 2.0 新增的 StateObject 數(shù)據(jù)流原語讓 SwiftUI 在重復(fù)創(chuàng)建 View 時避免重復(fù)創(chuàng)建 ObservedObject 從而提高 View 重建的性能。
SceneStorage 和 APPStorgae 讓一些可持久化的數(shù)據(jù)變得更加簡單且具有語義化。
New Controls
前面提到的,新增的 DSL 語法 SwiftUI App Lifecycle,以及 Xcode Library Preview 其實(shí)本質(zhì)上都是對去年 SwiftUI 1.0 錦上添花的新擴(kuò)展。
真正重要的是今年新增的各類新控件,其中通過導(dǎo)出來自 Xcode11.5 和 Xcode12.0 beta 版本的 Swift 聲明文件,可以觀察到整個聲明文件從原來的 10769 行增加到 20564行。
新增了約 87 個 struct 16 個 protocol。有了這些豐富的組件才可以更好的構(gòu)建我們的 APP 。
大列表組件
在任何一款 APP 中都會存在類似大列表組件,如淘寶 APP 里面的某家店鋪里面商品列表流,首頁的信息流,都是具有超長內(nèi)容的列表頁數(shù)據(jù)。對于長列表頁來說,過長的 UI 頁面會導(dǎo)致過多的內(nèi)存占用,在用戶的設(shè)備中,內(nèi)存是最為重要的指標(biāo),對于目前國內(nèi)的 APP 市場,低端手機(jī)仍然占據(jù)大量的市場,對于這些設(shè)備來說,一旦內(nèi)存超標(biāo),APP 就很容易 OOM,這會導(dǎo)致用戶體驗非常差,在現(xiàn)有競爭關(guān)系激烈的市場環(huán)境下,體驗差意味著會失去用戶。
對于傳統(tǒng)的命令式編程來說,我們可以主動控制 UITableViewCell 的重用,自建緩沖池等一系列手段去優(yōu)化我們的 APP 內(nèi)存占用,但是對于 SwiftUI 1.0 來說,系統(tǒng)提供的控件并沒有有效的辦法去讓我們控制頁面的渲染,對于大列表頁面就容易出現(xiàn)內(nèi)存占用過高的問題。
SwiftUI 2.0 推出了 LazyHStack 和 lazyVStack 加上 List 渲染模式默認(rèn)就是 Lazy 的直接解決了最大的性能問題。
筆者以去年使用 SwiftUI 編寫的 Emas App 為例,當(dāng)列表頁(并無大圖)加載到 500個時, APP 使用內(nèi)存已經(jīng)達(dá)到了將近 360MB 。而只需要切換到 Xcode12 API 調(diào)整為到 LazyVStack 內(nèi)存占用直接降低 300MB 。
Widget and Clips
蘋果與 WWDC 20 推出的 WidgetKit 支持的 API 是 SwiftUI Only,雖然已經(jīng)可以混合部分UIkit 里面的View,但相信沒有歷史包袱 最低支持版本為 iOS14 的 Widget 沒有人會選擇笨重的命令式 API。
同理 Clips 也一樣。
這里因為篇幅原因就不做展開,后續(xù)會有專門的文章分析相關(guān)技術(shù)。
Swift & SwiftUI 的機(jī)會在哪里?
筆者曾經(jīng)在公司推動集團(tuán)升級了基建,支持了 Swift 開發(fā)環(huán)境也在淘寶落地了一些場景,但是集團(tuán)內(nèi)一直有一些質(zhì)疑的聲音, 引入 Swift 到底有什么用?
SwiftUI 又是 N 年后才可以用上的小玩意,Objective-C 不夠用嗎?現(xiàn)在筆者可以回答這些質(zhì)疑的聲音, Swift 未來的機(jī)會在 效率,體驗和蘋果的技術(shù)紅利。
效率
從研發(fā)效率上來說, Swift 對比 Objective-C 的精簡程度不言而喻,筆者在淘寶 APP 上線的模塊代碼量下降了 40 %。 但更進(jìn)一步,如果編寫 UI 界面從 UIKit 轉(zhuǎn)向了 SwiftUI 代碼量直接少了不止一倍。更少的代碼意味著更快的交付,在目前競爭激烈的市場會有更多的試錯場景。關(guān)于使用 UIKit 編寫代碼轉(zhuǎn)向 SwiftUI 的代碼量對比,讀者可以參考開源 APP MovieSwiftUI 直觀了解。
體驗
讀者可能比較困惑對于切換語言和框架,對體驗看上去沒有任何幫助,但事實(shí)真是這樣嗎?
首先引入 Swift 后,由于 Swift 語言設(shè)計之初便對安全性列為最重要的目標(biāo),Swift的引入會讓代碼盡可能的減少未定義的行為,減少 Crash 意味著APP的穩(wěn)定性提高,體驗自然更佳。
其次雖然 Swift 同樣的語言出于對安全性考慮編譯處理的指令會比 Objective-C 更多,但是如果UI部分都用 SwiftUI 來寫呢?
更少的代碼意味者更小的包大小,目前國內(nèi)巨頭 APP iOS 端 APP 包大小都朝著 200 MB 奔去,如果能減少更多的代碼對包大小也可以在 200MB 的限制下承載更多而業(yè)務(wù)。對用戶的體驗也有較大的提升。
更進(jìn)一步由于 Swift 選擇使用值類型構(gòu)建整個APP,值類型的有點(diǎn)在于更扁平化的內(nèi)聯(lián)數(shù)據(jù)結(jié)構(gòu)去分配內(nèi)存,而不是使用更多間接指針引用,減少了大量不必要的堆內(nèi)存消耗,意味著整體內(nèi)存使用量的降低。對整個 APP 的穩(wěn)定性也有較大的提高。
? 蘋果的選擇
Swift 做為蘋果的戰(zhàn)略語言已經(jīng)發(fā)展的越來越壯大,自 2019 年 Swift ABI 穩(wěn)定后,蘋果在 Swift 的投入越來越大。我們可以進(jìn)入 /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/swift , /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks , https://github.com/apple 和 https://github.com/swift-server 看到, 自 iOS 13 以來 蘋果新增了約 10+ Pure Swift Library , 10+ Open Source Swift Library, 以及針對 144 個公開 Framework,根據(jù) Swift Style 重新設(shè)計了 57 個 Framework 的API。
從以下數(shù)據(jù):
從 WWDC17 后 蘋果已經(jīng)不再使用 Objective-C 做 Sample Code 演示
https://developer.apple.com/不再更新 Objective-C 相關(guān)的文檔
WidgetKit 是 SwiftUI only。
App Clips 10M的包大小, SwiftUI 是最合適的框架
開源社區(qū)逐步放棄 Objecive-C 如 Lottie。
可以判斷,Swift 是未來 Apple 平臺的唯一選擇,越是有包袱的大廠 APP,從現(xiàn)在還不盡早儲備,在未來越會寸步難行。
我們需要做些什么?
? Swift我們已經(jīng)做了什么
一套支持 Swift 二進(jìn)制的研發(fā)環(huán)境
300+ 支持了混編的淘系 SDK。
手淘落地了 6 個模塊。
集團(tuán)新增了約 20個 支持 Swift 的APP。
10 多場技術(shù)培訓(xùn)。
169+ 語雀知識沉淀。
300+ 工程師的集團(tuán) Swift 官方組織。
2 個 技術(shù)創(chuàng)新產(chǎn)品
經(jīng)過去年橫向組織大家共同的努力,我們已經(jīng)已經(jīng)支持了橫向大基建。包括研發(fā)環(huán)境,工具支撐,沉淀了大量的文檔,還有相關(guān)的技術(shù)課程。
要朝什么方向去努力
目前集團(tuán)對于 Swift 的呼聲越來越高,我們大量的工程師希望的去使用 Swift 。
目前首先要做的事情是依托 Swift 和 SPM 提升我們的開發(fā)體驗,升級我們的中間件,使業(yè)務(wù)可以大量的用起來 Swift ,提高我們的研發(fā)效率和代碼質(zhì)量。
升級基于 SPM 的新的包管理體系
升級老舊基礎(chǔ)庫,打磨新一代基建。
引入新的 Swift 特有庫 賦能業(yè)務(wù)。
? SwiftUI雖然前文提到了 SwiftUI 的眾多優(yōu)點(diǎn),包括研發(fā)效率,體驗的提高,但是在國內(nèi)的環(huán)境中 SwiftUI 也有它致命的弊端
iOS 14 才可放心的使用。
只支持 Apple Platform,這和國內(nèi)的要支持 Mobile Platform 從理念上沖突。
大型 APP 要解決的是如何部署到低版本操作系統(tǒng)上和安卓平臺上,畢竟很多公司還在支持 iOS 9 對于升級到最低支持 iOS 14 好像還需要一個世紀(jì)那么漫長,而且國內(nèi)的設(shè)備占比大頭還是以 Android 巨多 。
雖然可以看到 Swift 語言也在逐漸支持 Android 平臺,但是也看到蘋果對于安卓平臺的 SwiftUI 并沒有太大興趣。
從體驗上 Flutter 遠(yuǎn)不如 SwiftUI 這種親兒子效果好, 但對于國內(nèi)跨端欲望旺盛的市場來說 SwiftUI 還是比不過 Flutter, 不過既然 SwiftUI DSL 層已經(jīng)基本固定,那么也有可能投入人力直接在低版本操作系統(tǒng)上實(shí)現(xiàn)一套自建的 SwiftUI 引擎,或者將 SwiftUI 引擎移植到安卓平臺,比如對接 Flutter 或者直接對接 Android Native。
比起 Flutter 引入雙端帶來的包大小增量和體驗不一致的情況, SwiftUI 保留 iOS 平臺體驗,只侵入一端的選擇顯然要更好一點(diǎn)。
不過短期內(nèi)我們可以在 Clips 和 Widget 場景下開始使用 SwiftUI, 畢竟 SwiftUI 快速的開發(fā)效率對和較低的包大小占用非常適合這樣的場景,我們可以在業(yè)務(wù)場景中練兵儲備我們的 SwiftUI,并積極在主 APP 中嘗試。
