原創(chuàng):有趣知識點摸索型文章
創(chuàng)作不易,請珍惜,之后會持續(xù)更新,不斷完善
個人比較喜歡做筆記和寫總結(jié),畢竟好記性不如爛筆頭哈哈,這些文章記錄了我的IOS成長歷程,希望能與大家一起進步
溫馨提示:由于簡書不支持目錄跳轉(zhuǎn),大家可通過command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容
目錄
- 一、聲明式的界面開發(fā)方式
- 二、預(yù)覽
- 三、some關(guān)鍵詞的解釋
- 四、ViewBuilder的解釋
- 五、鏈?zhǔn)秸{(diào)用修改 View 的屬性原理
- 六、List的解釋
- 七、@State的解釋
- 八、Animating的解釋
- 九、生命周期
- Demo
- 參考文獻
簡介
SwiftUI 的最低支持的版本是iOS 13,可能想要在實際項目中使用,還需要等待一兩年時間。在 view的描述表現(xiàn)力上和與 app 的結(jié)合方面,SwiftUI 要勝過 Flutter和 Dart的組合很多。Swift雖然開源了,但是 Apple對它的掌控并沒有減弱。Swift 的很多特性幾乎可以說都是為了SwiftUI量身定制的。
另外,Apple 在背后使用Combine.framework 這個響應(yīng)式編程框架來對 SwiftUI.framework進行驅(qū)動和數(shù)據(jù)綁定,相比于現(xiàn)有的RxSwift/RxCocoa或者是ReactiveSwift 的方案來說,得到了語言和編譯器層級的大力支持。
一、聲明式的界面開發(fā)方式
描述「UI 應(yīng)該是什么樣子」而不再是用一句句的代碼來指導(dǎo)「要怎樣構(gòu)建 UI」。比如傳統(tǒng)的 UIKit,我們會使用這樣的代碼來添加一個 Hello World 的標(biāo)簽,它負(fù)責(zé)創(chuàng)建 label,設(shè)置文字并將其添加到 view 上。
func viewDidLoad()
{
super.viewDidLoad()
let label = UILabel()
label.text = "Hello World"
view.addSubview(label)
// 省略了布局的代碼
}
而相對起來,使用SwiftUI我們只需要告訴SDK需要一個文字標(biāo)簽。
var body: some View
{
Text("Hello World")
}
接下來,框架內(nèi)部讀取這些view 的聲明,負(fù)責(zé)將它們以合適的方式繪制渲染。注意,這些 view的聲明只是純數(shù)據(jù)結(jié)構(gòu)的描述,而不是實際顯示出來的視圖,因此這些結(jié)構(gòu)的創(chuàng)建并不會帶來太多性能損耗。相對來說,將描述性的語言進行渲染繪制的部分是最慢的,這部分工作將交由框架以黑盒的方式為我們完成。
如果 View 需要根據(jù)某個狀態(tài) (state) 進行改變,那我們將這個狀態(tài)存儲在變量中,并在聲明view時使用它。
@State var name: String = "Tom"
var body: some View
{
Text("Hello \(name)")
}
狀態(tài)發(fā)生改變時,框架重新調(diào)用聲明部分的代碼,計算出新的view 聲明,并和原來的 view 進行比較,之后框架負(fù)責(zé)對變更的部分進行高效的重新繪制。
二、預(yù)覽
SwiftUI 的 Preview 是 Apple 用來對標(biāo) Flutter 的 Hot Reloading 的開發(fā)工具。Xcode 將對代碼進行靜態(tài)分析 (得益于 SwiftSyntax 框架),找到所有遵守 PreviewProvider 協(xié)議的類型進行預(yù)覽渲染。另外,你可以為這些預(yù)覽提供合適的數(shù)據(jù),這甚至可以讓整個界面開發(fā)流程不需要實際運行 app 就能進行。
這套開發(fā)方式帶來的效率提升相比 Hot Reloading 要更大。Hot Reloading 需要你有一個大致界面和準(zhǔn)備相應(yīng)數(shù)據(jù),然后運行 app,停在要開發(fā)的界面,再進行調(diào)整。如果數(shù)據(jù)狀態(tài)發(fā)生變化,你還需要restart app才能反應(yīng)。SwiftUI 的 Preview 相比起來,不需要運行app并且可以提供任何的假數(shù)據(jù),在開發(fā)效率上更勝一籌。
三、some關(guān)鍵詞的解釋
struct ContentView: View
{
var body: some View
{
Text("Hello World")
}
}
一眼看上去可能會對 some 比較陌生,為了講明白這件事,我們先從 View 說起。View 是 SwiftUI 的一個最核心的協(xié)議,代表了一個屏幕上元素的描述。這個協(xié)議中含有一個associatedtype。
public protocol View : _View
{
associatedtype Body : View
var body: Self.Body { get }
}
這種帶有 associatedtype 的協(xié)議不能作為類型來使用,而只能作為類型約束使用。
Error
func createView() -> View
{
...
}
OK
func createView<T: View>() -> T
{
...
}
想要 Swift 幫助自動推斷出 View.Body 的類型的話,我們需要明確地指出body的真正的類型。在這里,body 的實際類型是 Text。
struct ContentView: View
{
var body: Text
{
Text("Hello World")
}
}
當(dāng)然我們可以明確指定出 body的類型,但是這帶來一些麻煩。每次修改body 的返回時我們都需要手動去更改相應(yīng)的類型。新建一個View 的時候,我們都需要去考慮會是什么類型。
其實我們只關(guān)心返回的是不是一個 View,而對實際上它是什么類型并不感興趣。some View 這種寫法使用了 Swift 的新特性 Opaque return types 。它向編譯器作出保證,每次 body 得到的一定是某一個確定的遵守 View 協(xié)議的類型,但是請編譯器網(wǎng)開一面,不要再細(xì)究具體的類型。返回類型確定單一這個條件十分重要,比如,下面的代碼也是無法通過的。
var body: some View
{
if someCondition
{
// 這個分支返回 Text
return Text("Hello World")
}
else
{
// 這個分支返回 Button,和 if 分支的類型不統(tǒng)一
return Button(action: {}) {
Text("Tap me")
}
}
}
這是一個編譯期間的特性,在保證associatedtype protocol的功能的前提下,使用 some 可以抹消具體的類型。
四、ViewBuilder的解釋
創(chuàng)建 Stack 的語法很有趣。
VStack(alignment: .leading)
{
Text("Turtle Rock")
.font(.title)
Text("Joshua Tree National Park")
.font(.subheadline)
}
一開始看起來好像我們給出了兩個 Text,似乎是構(gòu)成的是一個類似數(shù)組形式的 [View],但實際上并不是這么一回事。這里調(diào)用了 VStack 類型的初始化方法。
public struct VStack<Content> where Content : View
{
init(
alignment: HorizontalAlignment = .center,
spacing: Length? = nil,
@ViewBuilder content: () -> Content)
}
前面的 alignment 和spacing 沒啥好說,最后一個 content 比較有意思。看簽名的話,它是一個() -> Content類型,但是我們在創(chuàng)建這個VStack 時所提供的代碼只是簡單列舉了兩個 Text,并沒有實際返回一個可用的 Content。
這里使用了 Swift 5.1 的另一個新特性 Funtion builders。如果你實際觀察 VStack 這個初始化方法的簽名,會發(fā)現(xiàn)content前面其實有一個@ViewBuilder標(biāo)記,而 ViewBuilder則是一個由 @_functionBuilder 進行標(biāo)記的 struct。
@_functionBuilder public struct ViewBuilder { /* */ }
使用 @_functionBuilder 進行標(biāo)記的類型 (這里的 ViewBuilder),可以被用來對其他內(nèi)容進行標(biāo)記 (這里用 @ViewBuilder 對 content 進行標(biāo)記)。被用function builder標(biāo)記過的 ViewBuilder 標(biāo)記以后,content 這個輸入的 function 在被使用前,會按照 ViewBuilder 中合適的 buildBlock 進行 build后再使用。如果你閱讀 ViewBuilder 的文檔,會發(fā)現(xiàn)有很多接受不同個數(shù)參數(shù)的 buildBlock 方法,它們將負(fù)責(zé)把閉包中一一列舉的 Text和其他可能的 View 轉(zhuǎn)換為一個 TupleView并返回。由此,content 的簽名() -> Content可以得到滿足。實際上構(gòu)建這個 VStack 的代碼會被轉(zhuǎn)換為類似下面這樣的等效偽代碼(不能實際編譯)。
VStack(alignment: .leading)
{ viewBuilder -> Content in
let text1 = Text("Turtle Rock").font(.title)
let text2 = Text("Joshua Tree National Park").font(.subheadline)
return viewBuilder.buildBlock(text1, text2)
}
當(dāng)然這種基于 funtion builder 的方式是有一定限制的。比如ViewBuilder 就只實現(xiàn)了最多十個參數(shù)的 buildBlock,因此如果你在一個 VStack中放超過十個View的話,編譯器就會不太高興。不過對于正常的 UI 構(gòu)建,十個參數(shù)應(yīng)該足夠了。如果還不行的話,你也可以考慮直接使用 TupleView 來用多元組的方式合并 View。
TupleView<(Text, Text)>
(
(Text("Hello"), Text("Hello"))
)
除了按順序接受和構(gòu)建 View 的 buildBlock 以外,ViewBuilder 還實現(xiàn)了兩個特殊的方法:buildEither 和 buildIf。它們分別對應(yīng) block 中的 if...else 的語法和 if 的語法。也就是說,你可以在 VStack里寫這樣的代碼。
var someCondition: Bool
VStack(alignment: .leading)
{
Text("Turtle Rock")
.font(.title)
Text("Joshua Tree National Park")
.font(.subheadline)
if someCondition
{
Text("Condition")
}
else
{
Text("Not Condition")
}
}
其他的命令式的代碼在 VStack 的 content 閉包里是不被接受的,比如下面這樣就不行。let 語句無法通過 function builder 創(chuàng)建合適的輸出。
VStack(alignment: .leading)
{
let someCondition = model.condition
if someCondition
{
Text("Condition")
}
else
{
Text("Not Condition")
}
}
五、鏈?zhǔn)秸{(diào)用修改 View 的屬性原理
var body: some View
{
Image("turtlerock")
.clipShape(Circle())
.overlay(
Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
}
可以試想一下,在 UIKit 中要動手形成這個效果的困難程度。我大概可以保證,99%的開發(fā)者很難在不借助文檔或者 copy paste 的前提下完成這些事情,但是在SwiftUI中簡直信手拈來,這點和Flutter很像。在創(chuàng)建 View 之后,用鏈?zhǔn)秸{(diào)用的方式,可以將View 轉(zhuǎn)換為一個含有變更后內(nèi)容的對象。比如復(fù)原一下上面的代碼。
let image: Image = Image("turtlerock")
let modified: _ModifiedContent<Image, _ShadowEffect> = image.shadow(radius: 10)
image 通過一個 .shadow 的 modifier,modified 變量的類型將轉(zhuǎn)變?yōu)?code>_ModifiedContent<Image, _ShadowEffect>。如果你查看 View 上的 shadow 的定義,它是這樣的。
extension View
{
func shadow(
color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33),
radius: Length, x: Length = 0, y: Length = 0)
-> Self.Modified<_ShadowEffect>
}
Modified 是 View 上的一個typealias,在struct Image: View的實現(xiàn)里,我們有:
public typealias Modified<T> = _ModifiedContent<Self, T>
_ModifiedContent 是一個SwiftUI的私有類型,它存儲了待變更的內(nèi)容,以及用來實施變更的 Modifier。
struct _ModifiedContent<Content, Modifier>
{
var content: Content
var modifier: Modifier
}
在 Content 遵守 View,Modifier遵守 ViewModifier 的情況下,_ModifiedContent 也將遵守 View,這是我們能夠通過 View 的各個 modifier extension 進行鏈?zhǔn)秸{(diào)用的基礎(chǔ)。
extension _ModifiedContent : _View
where Content : View, Modifier : ViewModifier
{
...
}
在 shadow 的例子中,SwiftUI 內(nèi)部會使用 _ShadowEffect這個 ViewModifier,并把image自身和 _ShadowEffect 實例存放到_ModifiedContent 里。不論是 image 還是 modifier,都只是對未來實際視圖的描述,而不是直接對渲染進行的操作。在最終渲染前,ViewModifier 的 body(content: Self.Content) -> Self.Body將被調(diào)用,以給出最終渲染層所需要的各個屬性。
六、List的解釋
a、靜態(tài)List
這里的 List 和 HStack 或者 VStack 之類的容器很相似,接受一個view builder并采用聲明的方式列舉了兩個 LandmarkRow。這種方式構(gòu)建了對應(yīng)著UITableView的靜態(tài)cell的組織方式。
var body: some View
{
List
{
LandmarkRow(landmark: landmarkData[0])
LandmarkRow(landmark: landmarkData[1])
}
}
我們可以運行 app,并使用Xcode 的 View Hierarchy 工具來觀察 UI,結(jié)果可能會讓你覺得很眼熟。實際上在屏幕上繪制的 UpdateCoalesingTableView 是一個 UITableView 的子類,而兩個 ListCoreCellHost也是 UITableViewCell 的子類。對于 List 來說,SwiftUI 底層直接使用了成熟的UITableView 的一套實現(xiàn)邏輯,而并非重新進行繪制。
不過在使用 SwiftUI 時,我們首先需要做的就是跳出 UIKit 的思維方式,不應(yīng)該去關(guān)心背后的繪制和實現(xiàn)。使用 UITableView 來表達List也許只是權(quán)宜之計,也許在未來也會被另外更高效的繪制方式取代。由于SwiftUI層只是 View 描述的數(shù)據(jù)抽象,因此和Flutter 的Widget 一樣,背后的具體繪制方式是完全解耦合,并且可以進行替換的。這為今后 SwiftUI更進一步留出了足夠的可能性。
b、動態(tài) List
List(landmarkData.identified(by: \.id))
{ landmark in
LandmarkRow(landmark: landmark)
}
除了靜態(tài)方式以外,List 當(dāng)然也可以接受動態(tài)方式的輸入,這時使用的初始化方法和上面靜態(tài)的情況不一樣。
public struct List<Selection, Content> where Selection : SelectionManager, Content : View
{
public init<Data, RowContent>(
_ data: Data, action: @escaping (Data.Element.IdentifiedValue) -> Void,
rowContent: @escaping (Data.Element.IdentifiedValue) -> RowContent)
where
Content == ForEach<Data, Button<HStack<RowContent>>>,
Data : RandomAccessCollection,
RowContent : View,
Data.Element : Identifiable
//...
}
Content == ForEach<Data, Button<HStack<RowContent>>>
因為這個函數(shù)簽名中并沒有出現(xiàn) Content,Content 僅只 List<Selection, Content> 的類型聲明中有定義,所以在這與其說是一個約束,不如說是一個用來反向確定 List 實際類型的描述。
Data : RandomAccessCollection
這基本上等同于要求第一個輸入?yún)?shù)是 Array。
RowContent : View
對于構(gòu)建每一行的 rowContent 來說,需要返回是 View 是很正常的事情。注意 rowContent 其實也是被 @ViewBuilder 標(biāo)記的,因此你也可以把 LandmarkRow 的內(nèi)容展開寫進去。不過一般我們會更希望盡可能拆小 UI 部件,而不是把東西堆在一起。
Data.Element : Identifiable
要求 Data.Element (也就是數(shù)組元素的類型) 上存在一個可以辨別出某個實例的滿足 Hashable 的id。這個要求將在數(shù)據(jù)變更時快速定位到變化的數(shù)據(jù)所對應(yīng)的 cell,并進行 UI 刷新。
c、List : View的困惑
在下面的代碼中,我們期望 List 的初始化方法生成的是某個類型的 View。但是你看遍 List 的文檔,都找不到 List : View 之類的聲明。
var body: some View
{
List
{
//...
}
}
難道是因為 SwiftUI 做了什么手腳,讓本來沒有滿足 View 的類型都可以充當(dāng)一個 View 嗎?當(dāng)然不是這樣…如果你在運行時暫定 app 并用lldb打印一下List的類型信息,可以看到下面的信息。
(lldb) type lookup List
...
struct List<Selection, Content> : SwiftUI._UnaryView where ...
SwiftUI視圖_UnaryView協(xié)議雖然是滿足 View 的,但它被隱藏起來了,而滿足它的 List雖然是 public的,但是卻可以把這個協(xié)議鏈的信息也作為內(nèi)部信息隱藏起來。這是Swift內(nèi)部框架的特權(quán),第三方的開發(fā)者無法這樣在在兩個public的聲明之間插入一個私有聲明。
七、@State的解釋
這里出現(xiàn)了兩個以前在 Swift 里沒有的特性:@State 和 $showFavoritesOnly。
@State var showFavoritesOnly = true
Toggle(isOn: $showFavoritesOnly)
{
Text("Favorites only")
}
如果你點到 State 的定義里面,可以看到它其實是一個特殊的struct。
@propertyWrapper public struct State<Value> : DynamicViewProperty, BindingConvertible
{
/// Initialize with the provided initial value.
public init(initialValue value: Value)
/// The current state value.
public var value: Value { get nonmutating set }
/// Returns a binding referencing the state value.
public var binding: Binding<Value> { get }
/// Produces the binding referencing this state value
public var delegateValue: Binding<Value> { get }
}
@propertyWrapper標(biāo)注和@_functionBuilder 類似,它修飾的struct可以變成一個新的修飾符并作用在其他代碼上,來改變這些代碼默認(rèn)的行為。這里 @propertyWrapper修飾的 State被用做了 @State 修飾符,并用來修飾 View中的 showFavoritesOnly 變量。
和 @_functionBuilder 負(fù)責(zé)按照規(guī)矩重新構(gòu)造函數(shù)的作用不同,@propertyWrapper 的修飾符最終會作用在屬性上,將屬性包裹起來,以達到控制某個屬性的讀寫行為的目的。如果將這部分代碼展開,它實際上是這個樣子的。
// @State var showFavoritesOnly = true
var showFavoritesOnly = State(initialValue: true)
// Toggle(isOn: $showFavoritesOnly)
Toggle(isOn: showFavoritesOnly.binding)
// if !self.showFavoritesOnly
if !self.showFavoritesOnly.value
把變化之前的部分注釋了一下,并且在后面一行寫上了展開后的結(jié)果??梢钥吹?@State 只是聲明State struct的一種簡寫方式而已。State 里對具體要如何讀寫屬性的規(guī)則進行了定義。對于讀取,非常簡單,使用 showFavoritesOnly.value 就能拿到 State 中存儲的實際值。而原代碼中 $showFavoritesOnly 的寫法也只不過是 showFavoritesOnly.binding 的簡化。binding 將創(chuàng)建一個 showFavoritesOnly 的引用,并將它傳遞給 Toggle。再次強調(diào),這個 binding 是一個引用類型,所以 Toggle 中對它的修改,會直接反應(yīng)到當(dāng)前 View 的 showFavoritesOnly 去設(shè)置它的 value。而 State 的 value didSet 將觸發(fā) body 的刷新,從而完成 State -> View的綁定。
SwiftUI 中還有幾個常見的 @ 開頭的修飾,比如 @Binding,@Environment,@EnvironmentObject等,原理上和 @State 都一樣,只不過它們所對應(yīng)的 struct中定義讀寫方式有區(qū)別。它們共同構(gòu)成了 SwiftUI數(shù)據(jù)流的最基本的單元。
八、Animating的解釋
直接在 View 上使用 .animation或者使用 withAnimation { }來控制某個 State,進而觸發(fā)動畫。
button(action: {
self.showDetail.toggle()
}) {
Image(systemName: "chevron.right.circle")
.imageScale(.large)
.rotationEffect(.degrees(showDetail ? 90 : 0))
.animation(nil)
.scaleEffect(showDetail ? 1.5 : 1)
.padding()
.animation(.spring())
}
對于只需要對單個 View 做動畫的時候,animation(_:)要更方便一些,它和其他各類 modifier 并沒有太大不同,返回的是一個包裝了對象View 和對應(yīng)的動畫類型的新的 View。animation(_:)接受的參數(shù) Animation并不是直接定義 View 上的動畫的數(shù)值內(nèi)容的,它是描述的是動畫所使用的時間曲線、動畫的延遲等這些和 View 無關(guān)的東西。具體和 View 有關(guān)的,想要進行動畫的數(shù)值方面的變更,由其他的諸如 rotationEffect 和 scaleEffect 這樣的 modifier來描述。
要注意,SwiftUI 的 modifier 是有順序的。在我們調(diào)用 animation(_:)時,SwiftUI做的事情等效于是把之前的所有 modifier 檢查一遍,然后找出所有滿足 Animatable 協(xié)議的view上的數(shù)值變化,比如角度、位置、尺寸等,然后將這些變化打個包,創(chuàng)建一個事物(Transaction)并提交給底層渲染去做動畫。在上面的代碼中,.rotationEffect后的 .animation(nil)將rotation的動畫提交,因為指定了nil所以這里沒有實際的動畫。在最后,.rotationEffect已經(jīng)被處理了,所以末行的.animation(.spring()) 提交的只有.scaleEffect。
在withAnimation { } 閉包內(nèi)部,我們一般會觸發(fā)某個 State 的變化,并讓View.body進行重新計算.
Button(action: {
withAnimation
{
self.showDetail.toggle()
}
}) {
//...
}
如果需要,你也可以為它指定一個具體的 Animation。這個方法相當(dāng)于把一個animation設(shè)置到 View 數(shù)值變化的 Transaction 上,并提交給底層渲染去做動畫。從原理上來說,withAnimation 是統(tǒng)一控制單個的 Transaction,而針對不同 View 的 animation(_:)調(diào)用則可能對應(yīng)多個不同的 Transaction。
withAnimation(.basic())
{
self.showDetail.toggle()
}
九、生命周期
在 UIKit 開發(fā)時,我們經(jīng)常會接觸一些像是 viewDidLoad,viewWillAppear這樣的生命周期的方法,并在里面進行一些配置。SwiftUI里也有一部分這類生命周期的方法,比如.onAppear 和.onDisappear,它們也被統(tǒng)一在了 modifier 這面大旗下。
但是相對于UIKit來說,SwiftUI中能使用的生命周期方法比較少,而且相對要通用一些。本身在生命周期中做操作這種方式就和聲明式的編程理念有些相悖。個人比較期待View 和 Combine能再深度結(jié)合一些,把依賴生命周期的操作也用綁定的方式搞定。
相比于.onAppear 和.onDisappear,更通用的事件響應(yīng)是.onReceive(_:perform:),它定義了一個可以響應(yīng)目標(biāo) Publisher的任意的 View,一旦訂閱的 Publisher發(fā)出新的事件時,onReceive就將被調(diào)用。
Demo
Demo在我的Github上,歡迎下載。
SwiftUIDemo