Qunar SwiftUI 的實踐、評測與思考

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f5/f5234ac9b7024f7b6dec82e5ed9f9b1b.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"趙龍,2020年加入Qunar,擔任大前端iOS開發,OC、 SWIFT、 C++、Dart等技能豐富,喜歡優化開發流程,研究增加效率的代碼和開發方式。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/73/7389369f6bbb502bebd1b629ae63a1a4.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"林書輝,2018年加入Qunar,iOS、RN開發工程師。目前負責大客戶端公共產品首頁、用戶中心等功能的開發和維護。持續關注學習前沿的大前端技術,推崇技術創新帶來的效率優化和性能提升。現致力於Native+DSL動態化組件方案的開發與推廣。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一. 前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SwiftUI 出現已經2年,至今尚未大規模推廣落地,它侷限在 iOS 生態內,暫時閉源的 UI 庫,需要 iOS13 版本來適配,這些因素阻礙了更多人使用,但是其實它相對於其他UI框架具有非常高的開發效率與運行效率,相對於 Objective-C+UIKit 更是一個全面的框架升級。寫這篇文章是爲了讓大家熟悉 SwiftUI ,讓客戶端同學在技術選型的時候有切實的數據和特性來參考,也希望推進大客戶端的 Swift 基礎設施建設。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二. SwiftUI 代碼庫","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"iOS 中 SwiftUI 由2個底層框架驅動 SwiftUI.framework 與 Combine.framework其中 SwiftUI.framework 負責界面搭建,簡潔的 DSL 相比 OC 讓開發效率提升不少,例如我們要實現在屏幕中心實現一個帶文本按鈕, OC 中我們一般這樣寫:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"objectivec"},"content":[{"type":"text","text":"- (void)viewDidLoad {\n CGRect screen = [[UIScreen mainScreen] bounds];\n UIButton * centerBtn = [UIButton buttonWithType:UIButtonTypeCustom];\n [centerBtn setFrame:CGRectMake(screen.size.width / 2 - 30, screen.size.height / 2 - 15, 60, 30)];\n [centerBtn setTitle:@\"測試按鈕\" forState:UIControlStateNormal];\n [self.view addSubview:centerBtn];\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而在 SwiftUI 中我們這樣寫:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"var body: some View {\n Button(){\n Text(\"測試按鈕\")\n .frame(width: 60, height: 30, alignment: .center)\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Combine.framework 負責生成數據流綁定操作與界面,基於觀察者模式,當一個對象的狀態發生改變時,所有依賴於它的對象都得到通知並被自動更新。只不過,這些框架對這個模式進行了一點擴充,在被觀察者與觀察者之間引入了可選的轉換操作(操作符:Operators)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三. 渲染流程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以我們去哪兒小組件一個頁面左下角文本的渲染爲例。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/47/47a5a0d6a6200c7216badd9e2fbe56a5.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"Text(self.title)\n .font(Font.system(size:14))\n .fontWeight(.semibold)\n .foregroundColor(.white)\n .frame(alignment: .center)\n .offset(x: -3.5, y: 2)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進入頁面時每個元素(例子中的 Text )由代碼底層開始,每個單元(每個.開頭的一行)進行鏈式組裝返回一個 view ,之後通知它的上級單元(代碼中的上面一行),把下面單元生成的 view 當做參數,生成新的 view ,遞歸形成整個渲染樹,進入流水線進行渲染。當狀態發生變化時,首先會對比前後 View 聲明的變化, SwiftUI 只會向流水線提交聲明中不同的部分。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四. 生命週期","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WWDC 2020期間,蘋果公佈 SwiftUI 獲得了新的的應用程序生命週期,以擺脫 UIKit 的AppDelegate 和 SceneDelegate 的舊模式。基於此,iOS 14現在提供了一個 App 協議、一個SceneBuilder、 scenePhase枚舉器和一個新的屬性包裝器UIApplicationDelegateAdaptor 。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"遵循 APP 協議,我們可以用新的方式構建UI,其中必須實現 SceneBuilder ,它本質上是一個用於構建一個或多個場景的函數構建器,使用 scenePhase 來獲取場景當前狀態,UIApplicationDelegateAdaptor用來連接老的app生命週期方法。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"新的生命週期代碼執行順序爲:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/80/805293a90024ce1b05dc68e509c54d44.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相對 AppDelegate,它使用 WindowGroup 包裝在需要監聽生命週期的場景之外,可以方便快捷的訪問頁面生命週期方法,簡潔而且可以讓頁面之間解耦。現有的渲染流程仍然遵循 iOS Cocoa 框架,使用 Core Animation 爲渲染核心庫。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"五. 語言特點","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SwiftUI 從根本上不是在構建 UI,而是在描述 UI 。這兩者有什麼區別?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"構建 UI 時,你會明確每一個控件的類型,甚至精確到平臺。比如在原生 UI 框架中,你需要一個輸入框 ,你就要根據不同的平臺,選擇具體是使用 UITextFiled(iOS) 還是 NSTextFiled(macOS),而這兩個輸入框 有着不同的屬性、外觀和特性。在 Flutter 中,你不需要考慮不同平臺的控件類型,但你需要考慮控件的風格,官方提供了符合 Material Design 的 Android 控件和 iOS Style 的 iOS 控件,它們的許多特性也不一致,這一切給 UI 構建帶來了更加複雜的邏輯和工作量。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而 SwiftUI 更像是 Web 的模版,它只描述 UI ,而控件具體長什麼樣子,有什麼特性,會根據編譯的平臺因地制宜的實現,而且是自動的。這就像是給 Web 套上了一個模版,一個主題。接下來展示一個控件Picker,相同的代碼在不同平臺上不同的表現。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ba/ba7e4b88548bc910eb800510777605e5.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7b/7b9c0b534ef052971db62970785baab8.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你會發現,同樣的代碼,展示出來的 UI 卻完全不一樣,但 UI 表達的內容確實完全一致的。Picker 控件,在iOS上被翻譯成了一個選擇頁面,有滾輪選擇效果。而在 macOS 上,Picker 則會被翻譯成我們桌面操作系統上常見的下拉列表。它完全符合了平臺設計語言和用戶使用習慣,同時又極大的降低了開發和適配難度。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從這一點上來看,SwiftUI 和 RN 有着更爲相似的思路和技術。只不過 Facebook 無論對 iOS 還是 Android 都幾乎沒有任何話語權,因此在兼容性、一致性上都跟SwiftUI或者將來的谷歌自有跨平臺框架有差距。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"六. SwiftUI 數據狀態和綁定","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SwiftUI分三種方式綁定數據與界面。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.@State & @Binding","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提供 View 內部的狀態存儲,應該是被標記爲 private 的簡單值類型,僅在內部使用。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2.ObservableObject & @ObservedObject","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對跨越 View 層級的狀態共享,處理更復雜的數據類型,在數據變化時觸發界面刷新。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"3.@EnvironmentObject","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於 “跳躍式” 跨越多個 View 層級的狀態,更方便地使用 ObservableObject,以簡化代碼。它們在使用時,都是通過在對應的屬性前加以上修飾符,使用Propertywrapper的包裝使得屬性成爲被觀察者,如果它們有變化,就會直接通知對應範圍內的UI刷新。可以看到,蘋果在 SwiftUI 引入了前端開發中漸成主流的響應式編程思想,開發者基於蘋果提供的原生 API ,就可以輕鬆實現視圖和邏輯處理的解耦,降低了代碼的維護成本,從而讓開發者可以將更多精力投入業務的實現。反觀目前 Qunar 正在使用的Objective-C+UIKit 的開發模式,如果開發者想要體驗響應式編程,則面臨着引入一系列第三方框架(如ReactiveCocoa)、代碼風格受語法限制等一系列問題。從這一點上, SwiftUI 相比於 Objective-C 的優勢是十分明顯的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"七. SwiftUI 的一些語法特性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.Opaque Result Type","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"靜態語言中返回對象類型必須在編譯時確定,而一些對象在代碼中又想隱藏自己的類型,對外使用協議類,對此 SwiftUI 給出了自己的解決方案,不透明返回類型,一句話概括,它就是一個泛型實例化的時候,不依賴調用者指定類型的一種語法特性。看一個例子:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"func reverseGeneric() -> some Shape { return Rectangle(...) }\nlet x = reverseGeneric()\n// type(of: x) == Rectangle\n// 並且 x 的類型根據 reverseGeneric 的具體實現決定","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2.Propertywrapper","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個特性可以在屬性 Get、Set 的時候,將部分可複用的代碼包裝起來,用一個@符接一個自己定義的屬性名稱來使用這種能力。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"3. FunctionBuilder","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個特性是一種語法糖,可以用方法參數接受隱式閉包構建複合視圖,換句話說就是可以在組件構建參數裏直接寫我們要構建的內容,而不需要各種語義控制符,例如冒號,逗號,聲明參數類型,閉包類型等。看一個例子:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"VStack(alignment: .leading) {\n Text(\"Hello, World\")\n Text(\"Longzhao.zhao\")\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"合理利用這些特性,在去哪兒iOS小組件中,用大約600行的代碼就實現了登錄,酒店,機票,火車票的狀態展示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"八. SwiftUI 比較其他聲明式 UI 框架","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"聲明式 UI 框架,帶給了開發者優秀體驗,相比於使用類似 iOS 中的 UIKit 等命令式 UI 框架搭建 UI 界面,聲明式 UI 框架讓開發者更像在使用自然語言描述自己的需求。從代碼維護的角度來看,一個複雜的 UI 結構,如果使用命令式 UI 框架,很難直觀的讓開發者一眼看出某段代碼想要實現一個什麼樣的 UI 效果,而規範的聲明式的代碼甚至會給人一種“所見即所得”之感。總結一下,目前業內普遍認爲聲明式 UI 較命令式UI理念上更爲先進的原因,主要表現在如下幾點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.適合做一次開發,多種不同的設備類型自適應。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.顯示UI和控制邏輯通過響應式思想與數據進行綁定,實現解耦。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.更易於實現UI控件的局部刷新機制。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們挑選了目前移動端比較主流的另外兩個聲明式UI框架 – Flutter、RN,與SwiftUI進行比較,希望可以給大家一些啓發。比較分四個維度:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"關於語法:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SwiftUI 得益於 Swift5.1 加入的 Function Builder 與 Opaque return types(非透明返回類型)特性,增強了構建內置 DSL 的能力,使代碼變得簡潔有效。配合FuctionBuilder、尾隨閉包(Trailing closure)、省略 return(當函數體中只有單獨一個表達式,就會自動添加一個 return,返回這個表達式的值)等 Swift 語言特性, SwiftUI 實現了直觀的代碼風格,效果如下:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"struct TripCardContentView: View {\n var cardData: CardData?\n var body: some View {\n switch cardData?.cardType {\n case CARD_TYPE_FLIGHT:\n FlightCard(cardData: self.cardData as? FlightData)\n case CARD_TYPE_TRAIN:\n TrainCard(cardData: self.cardData as? TrainData)\n case CARD_TYPE_HOTEL:\n HotelCard(cardData: self.cardData as? HotelData)\n default:\n Spacer()\n }\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終根據實際業務呈現的效果:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/30/30aefb35037eb5c3b09484a45c6aaed4.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/db/dbb2577787110d364c36e2f6f684cf72.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b0/b070decd63a57ad31e4d0250c21afed8.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RN使用 JSX 來模擬 HTML 標籤的方式,更符合 web 開發的思維,個人認爲各有優勢相比之下, Flutter 由於沒有如此強大的內部 DSL ,所以在開發體驗上會有一些不方便,雖然 Flutter 團隊通過編輯器的提示插件、保存時強行格式化等方式進行了一些開發體驗優化,但是總體上還是不如前兩者","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於這些語言特性我們大大加快了開發速度,小組件機票展示業務從零開始搭建 Swift 環境(包含工程適配,打包平臺支持)只花了7天就可以上線,酒店和車票展示業務隨後開發,代碼耗時 2pd 。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"關於熱加載:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SwiftUI 的實時預覽分爲靜態預覽和動態預覽。默認情況下展示的是靜態預覽,它的速度快,而且支持代碼和可視化兩種編寫方式。不過它沒有任何響應事件,無法滾動和跳轉頁面。如果需要動態調試,則需要切換到動態預覽。動態預覽需要經過一段編譯時間,然後可以以完全動態的形式實時響應 UI 的變化,而且可以在真機上實時調試。但動態預覽的限制還比較多,無法做到 Flutter 那種只需編譯一次,之後整個 App 都能實現動態更新。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"RN的基於HMR(Hot Module Replacement模塊熱替換機制)實現了hot reload,體驗還是很不錯的,但是在實際使用中,經常會出現代碼寫了一半,因爲編輯器自動保存觸發了watchMan的回調,走到了hot reload邏輯,從而造成紅屏。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Flutter是通過將更新後的源代碼文件注入正在運行的Dart虛擬機(VM)中來實現的熱重載。在虛擬機使用新的的字段和函數更新類後,Flutter框架會自動重新構建widget樹,以便開發者快速查看更改的效果。但是由於靜態字段惰性初始化等Dart語言特性,在一些場景下,如:更改全局變量和靜態字段的初始值設定項、枚舉類型更改爲常規類或常規類更改爲枚舉類型等,熱重載後會無效,需要完全重啓應用纔可以。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總體來講,三個框架的熱重載機制體驗差不多,都達到了可以實時預覽樣式的目的,這提升了搭建UI時的工作效率。但是對於一些涉及到邏輯的代碼變更,還是完全重載來的更爲穩妥。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"跨平臺方面:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前 SwiftUI 支持 iOS、watchOS、iOS14 小組件、MacOS 平臺的 APP 開發,理論上是可以一套代碼編譯出多個平臺的應用,但是基於用戶體驗的考慮,蘋果官方並不推薦爲不同的設備採用一套 UI 設計,他們更加推崇 “Learn once, apply anywhere” 。相比於 RN 和 Flutter 可以橫跨 OS、Android(Flutter最新release版還支持windows、MacOS、web平臺),SwiftUI 更關注於蘋果自身各個平臺的編碼統一性和體驗的提升,在這一點上,Swift 的理念和其他兩個是有一些區別的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"性能方面:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過超長列表視圖、大批量動畫同時播放、大量視圖旋轉和縮放等測試場景,我們得出了以下結論:在以上場景中,SwiftUI 不出預料確實具有最強的性能。其次就是 Flutter 和 RN。但是如果一定要考慮跨平臺的需求,在 CPU 佔用率很高的業務中應避免使用RN,相比之下 Flutter 更加適合這種任務。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b4/b41cfdb0bf289bc717dd7dcae61d89b7.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"九. SwiftUI在Qunar大客戶端落地場景的思考","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上我們從不同的維度比較了各種聲明式 UI 框架。我們認爲所有的技術都有適合它的場景,拋開場景來做選擇必然會帶來技術方案選型不合適的問題。下面我們也基於 Qunar 大客戶端存在的一些具體場景,進行了上面提及的技術的一些落地的思考,總結如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.對一些性能要求較高,同時爲了節約效率而傾向於跨平臺開發的頁面,可以使用 Flutter 進行開發。但是由於目前 Flutter 還沒有官方的動態化熱更新方案,所以新功能的迭代,還需要依賴發版來解決。關於 Flutter 框架在大客戶端的支持,公司的開發同事已經有了很大的進展,非常期待後續 Flutter 技術在公司全面應用。應用場景方面, IM 聊天會話頁,對頁面的 UI 一致性要求高,同時要保證繪製的效率,會是 Flutter 落地的理想場景.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.對一些性能要求較低,更傾向於與高效率跨平臺開發及快速迭代需要熱更新的頁面,可以選擇 RN 。這也是去哪兒目前比較主流的解決方案了,它的熱更新和越來越豐富的 API 爲客戶端的功能迭代帶來了很大的效率提升,但是由於 RN 框架的機制,註定了一些對性能有苛刻要求的頁面不得不去考慮其他方案。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.針對一些對性能有極高要求的頁面,而且可以接受 iOS 與安卓分別投入人力進行開發,並且跟版更新,如大客戶端首頁、登錄頁等,目前 native 仍然是不可替代的方案。native 端的UI實現可以考慮使用 UIKit 和 SwiftUI 框架進行開發,但是由於 SwiftUI的最低支持 iOS 版本爲 iOS13.0,而我們的大客戶端目前的最低支持版本爲 iOS10.0,所以在未來 iOS13+ 成爲絕對佔有率很高的系統之後,這些頁面可以考慮首選轉型爲 swiftUI 爲框架實現,它迎合了現在首頁的幾個需求:始終擁有最高幀率表現,能最大化降低首頁啓動時間,崩潰與卡頓定位最精確,保持 iOS  新特性最快最全面的支持","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4. Objective-C 無論開發還是運行效率都已落後,如果可以有最低版本爲 iOS13 的場景, SwiftUI 都是更好的選擇,老的 OC 框架只有在對接有歷史包袱的需求可能有一些優勢","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5.因爲 Swift 與 Objective-C 可以通過橋接互相調用,現階段讓 Swift 兼容老的 OC 、RN、Flutter 代碼最經濟的方式是使用Swift建橋鏈接必要的 OC API。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"6.目前我們已經使用 SwiftUI 框架實現了大客戶端 iOS14 小組件,從20年11月上線到現在,支撐起了 iOS 桌面待出行行程信息展示的高頻場景。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"十. Objective-C遷移Swift與SwiftUI","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Swift 語言是 iOS 開發的方向,我們也調研了向 Swift 與 SwiftUI 遷移的可行性,大部分OC代碼都可以直接找到對應API翻譯成Swift,有一些特性需要注意,例如@synchronized,dispatch_once 等在 Swift 中已經移除,需要自己寫一個類似功能來實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"舉一個自己實現這些特性的例子:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"swift"},"content":[{"type":"text","text":"//swift同步鎖\nfunc synchronized(_ lock: AnyObject, block: () -> Void) {\n objc_sync_enter(lock)\n block()\n objc_sync_exit(lock)\n}\n//swift dispatch_once\npublic extension DispatchQueue {\n private static var _onceTokens = String()\n public class func once(token: String, block:()->Void) {\n objc_sync_enter(self)\n defer { objc_sync_exit(self) }\n if _onceTokens.contains(token) {\n return\n }\n _onceTokens.append(token)\n block()\n }\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在有沒有將大客戶端的代碼全部遷移到 Swift 的必要呢?從遷移成本和收益上考慮,沒有太大必要,比較可行的方案是新增一部分功能的 Swift 框架,如網絡庫、appInfo、通用服務等,框架內部通過橋接封裝現有的 OC 框架,提供 Swift 版本的 api 供業務線使用。這樣後續 native 代碼實現的功能,會多一種 Swift 實現的選擇,從而實現逐步遷移。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"十一. 總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Swift 做爲蘋果的戰略語言已經發展的越來越壯大,自 2019 年 Swift ABI 穩定後,蘋果更是在全力支持 Swift 。我們可以進入蘋果源碼庫 看到, 自 iOS 13 以來 蘋果新增了約 10+個純 Swift 庫, 10+個開源 Swift庫, 以及針對 144 個公開 Framework,遵守 Swift 風格 重新設計了 57 個 Framework 的API。基於以下事實:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一.從 WWDC2017 後 蘋果已經不再使用 Objective-C 做代碼演示;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二.https://developer.apple.com/ 已經不再更新 Objective-C 相關的文檔;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第三.WidgetKit 只支持 Swift,沒有支持 Objective-C;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第四.App Clips 限制了包大小爲 10M, SwiftUI 是最合適的框架;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第五.開源社區中 Objecive-C 比例逐漸降低甚至廢棄 如 Lottie。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以判斷,Swift 是未來 Apple 平臺的唯一選擇,我們如果可以在 Native 開發中儘早甩掉包袱使用 Swift ,就可以在下一步與其他APP的開發效率及運行效率的比較中取得優勢。Qunar 大客戶端開發中我們可以根據以上列舉的數據和方案自行做出最適合需求的技術選型,選擇了 Swift 語言或者 SwiftUI 框架也可以這篇文章也可以對照這裏的實現細節作爲參考。去哪兒 iOS 大客戶端已經支持 Swift&OC 混編,各業務線可以直接加入 Swift 文件參與本地編譯和 CM 打包,以後我們也會積極推進建立一套大客戶端的 Swift 基礎框架來支持公司向 Swift 和 SwiftUI 方向發展。","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章