Apple 在 WWDC19 上正式發佈了 Project Catalyst(原 Marzipan),使得開發者可以將 iPadOS app 移植到 macOS 上。同時 SwiftUI 也壓軸亮相,正式統一了 Apple 全平臺的 UI 開發解決方案。恰逢前些時候,Google 在其 I/O 大會上亮相了 Jetpack Compose —— 一個全新的 Android 原生 UI 開發框架,標誌着兩大移動操做系統陣營全面擁抱聲明式 UI 開發模式。前端
其實聲明式 UI 並非什麼新技術,早在 2006 年,微軟就已經發布了其新一代界面開發框架 WPF,其採用了 XAML 標記語言,支持雙向數據綁定、可複用模板等特性。git
2010 年,由諾基亞領導的 Qt 團隊也正式發佈了其下一代界面解決方案 Qt Quick,一樣也是聲明式,甚至 Qt Quick 起初的名字就是 Qt Declarative。QML 語言一樣支持數據綁定、模塊化等特性,此外還支持內置 JavaScript,開發者只用 QML 就能夠開發出簡單的帶交互的原型應用。github
聲明式 UI 框架近年來飛速發展,而且被 Web 開發帶向高潮。React 更是爲聲明式 UI 奠基了堅實基礎並一直引領其將來的發展。隨後 Flutter 的發佈也將聲明式 UI 的思想成功帶到移動端開發領域...算法
想象咱們要實現下面這個界面:swift
打開開關就讓下面的 label 顯示 on,反之顯示 off。若是咱們要用非聲明式的方式實現,即命令式,那麼須要:xcode
UISwitch
,設置它的 change 事件 handlerUILabel
UIStackView
,設置方向爲垂直UIStackView
中這樣作面對一個狀態,咱們尚且可以正確處理,但隨着應用日漸複雜,狀態也愈來愈多而且錯綜複雜,狀態變化的順序甚至也能影響應用邏輯的正確性,由於咱們對每一個事件的處理都是對界面的增量修改。一旦前一個狀態有錯誤,後面就會錯上加錯,接下來多線程混入,而後 boom,你的應用可能就 crash 了。bash
聲明式的意思就是讓咱們描述咱們須要一個什麼樣的界面,而不是告訴計算機一步一步幹什麼。那麼上面的例子用聲明式就是這樣:前端框架
「我須要一個界面,它是一個 VStack(垂直佈局),裏面有一個開關,開關的值與 switchValue 的布爾值綁定,VStack 裏接下來是一個 Text,它的值當 switchValue 爲 true 時是 foo,不然是 bar」多線程
咱們能夠發現,全文沒有命令,都是在描述界面是怎樣的。switchValue
咱們稱之爲 「The Source of Truth」,Toggle 的狀態、Text 的文本內容都與它相綁定。狀態變化時,界面按照先前描述的從新「渲染」便可獲得狀態絕對正確的界面。這正是聲明式的優點所在,下降狀態增長時界面維護的複雜度。閉包
SwiftUI 自亮相以來,全網就在討論其與 React、Flutter 之間的關係云云。通過這兩天的研究,我想簡單談談個人觀點:(免責聲明:沒有看過源碼,也沒有參與現場 Lab,一切都是我的想法)
首先是與 Flutter 的對比,Flutter 的思路是從 0 開始,即語言、基礎庫、渲染引擎、排版引擎即框架自己所有由本身實現,其渲染引擎 Skia 只須要操做系統爲止提供一個 GL Context 即可以完成全部圖形渲染,這使得其跨平臺性變得十分強大,到目前爲止 Windows、Linux、macOS、Fuchsia 都已經獲得了 Flutter 官方的支持。
這種作法我認爲有利有弊,首先好處是全部平臺下行爲一致,不論是滾動視圖、Material Design 控件仍是模糊效果這些在其餘平臺沒有的都獲得了全平臺的支持,開發者並不須要爲這些去作平臺間的適配,反觀 React Native... 固然缺點也是存在的,Flutter 這種作法相似於遊戲引擎,平臺提供的 UI 特性它一律不用,所以 Flutter View 與原生視圖的交互就沒有那麼容易了,同時新的 Dart 語言貌似也不是很是受社區和開發者喜好。
SwiftUI 沒有像 Flutter 那樣從頭再來,這個全新的框架依舊使用了 UIKit、AppKit 等做爲基礎。但它並非一個 UIKit 的聲明式封裝,經過 Xcode 的調試視圖能夠看出這一點:
許多基礎組件,像 Text、Button 等都並非直接使用 UILabel
、UIButton
而是一個名爲 DisplayList.ViewUpdater.Platform.CGDrawingView
的 UIView
子類。它們使用了自定義繪製,但又承載於 UIKit 的環境中,所以我猜想 SwiftUI 只提供了組件的自定義渲染和佈局引擎,它使用到的底層技術仍是 Core Animation、Core Graphics、Core Text 等。使用自定義繪製去實現組件能夠理解成爲跨平臺提供便利,畢竟一個按鈕還要區分 UIButton
、NSButton
來實現未免有些麻煩。可是部分複雜的控件仍是採用了 UIKit 中已有的類,好比 UISwitch
等。因爲未脫離 UIKit 體系,嵌入一個 UIView 很是容易,你不須要搞什麼外部紋理(Flutter 須要),由於它們的上下文是同一個,座標系也是同一個。
因此我認爲 SwiftUI 更加相似 React Native,使用系統框架提供的組件,只不過繪製和佈局能夠本身來實現,這在 SwiftUI 以前也有相關的框架這樣實踐的,好比 Yoga、ComponentKit 等。
Flutter、React 的類型系統並非強約束,一個界面裏有一個 Text 和有兩個 Text 類型是同樣的,React 使用 JavaScript 更是無類型。SwiftUI 與它們不一樣,它使用了強類型約束。舉個例子:
VStack {
Text("Hello")
}
複製代碼
與
VStack {
Text("Hello")
Text("World")
}
複製代碼
與
VStack {
Text("Hello")
.color(Color.red)
}
複製代碼
類型都是不一樣的。首先上面這種語法叫作 Function Builders,是 Apple 「私自」夾帶到 Swift 裏的私貨。上面這些表達式最後都會獲得一個實現了 View
協議的具體類型,SwiftUI 裏基本使用的都是具體類型,而不是協議類型,首先 VStack
是一個 struct 同時也是一個具體類型,它的構造方法裏接受一個閉包,這個閉包使用了經過 @functionBuilder
修飾的 ViewBuilder
結構體做爲 builder,所以上面的第二段代碼在編譯時會被轉化成:
VStack {
let v1 = ViewBuilder.buildExpression(Text("Hello"))
let v2 = ViewBuilder.buildExpression(Text("World"))
return ViewBuilder.buildBlock(v1, v2)
}
複製代碼
而後咱們看一下上面這個 ViewBuilder.buildBlock
重載的簽名:
static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View
複製代碼
因此一個 Text 和兩個 Text,它們的父容器 VStack 的類型都是不一樣的!另外提一下,buildBlock
的範型參數最多有 10 個:
劃重點:也就是你的一個視圖層級(目前)不能有超過 10 個子視圖。 且超事後編譯器的錯誤提示絲絕不會體現這一點,瞭解這個將會很是節約你的時間!
不一樣的狀態對應的視圖也不一樣,可是它們的類型是相同的,這意味着什麼呢?那就是,不須要 Diff-Patch 了。
咱們想象下面的場景:
VStack {
if something {
Text("something is true")
}
Text("something else")
if !something {
Text("something is not true")
}
}
複製代碼
當 something
變化時,視圖應該怎麼變化?對於 React、Flutter 來講,它們沒有類型的概念,每次只能拿到兩個快照(一個當前狀態的,一個新狀態的)。它們有兩個選擇去完成界面的更新:
第一種方法最簡單,可是性能不好,且不能保存視圖自身的狀態。第二種方法須要高效的算法加持,看起來能解決咱們的問題,可是它不是必要的。
SwiftUI 的作法是根據類型來更新界面,上面這段代碼的類型是:
VStack<TupleView<Text?, Text, Text?>>
複製代碼
有了類型框架就能作靜態優化,這相似前端框架 Svelte 和 Vue.js 3.0 所作的一些優化,能夠稱之爲 AOT。
在沒有類型的狀況下,每次狀態變化,界面中都只有兩個 Text,只不過內容不同,這時候框架經過 diff 認爲界面中的 Text 控件自己沒變,只是內容變了,因而給它們設置了新的內容。
但事實並非這樣,something
變化時,界面顯示的 Text 是不一樣的,中間的 Text 始終顯示 「something else」,變化的是它上下兩個相鄰的 Text。框架拿到新視圖時就能夠按範型參數的順序去檢查他們的差別:
Before update:
VStack(TupleView(Text(...), Text(...), nil))
After update:
VStack(TupleView(nil, Text(...), Text(...)))
複製代碼
它們的相對位置寫在了類型中,這樣就能避免中間的視圖被修改,沒有類型信息或其餘元信息,這點是絕對作不到的。
SwiftUI 對於類型作得其實更多,全部的字體調整、位置調整等操做在 SwiftUI 中都是經過 ViewModifier
實現的,調整後的視圖類型爲 View.Modified<some ViewModifier>
,所以有無這些參數調整的視圖,類型也是不一樣的,這些都將有助於框架去作一些靜態優化。
關於 SwiftUI 的詳細使用方面,我以後可能還會再更新文章,本文就是簡單談談我對框架宏觀層面的理解。祝你們 WWDC 周玩得開心~
References: