一、概覽
本篇文章將概述 SwiftUI 的工作原理伪窖,以及它與 UIKit 等框架的不同之處回官。SwiftUI 在概念上與以前的 Apple 平臺上開發(fā) app 的方式完全不同甘畅,它需要你重新考慮如何將你腦中的構(gòu)想轉(zhuǎn)換為實際可工作的代碼列另。
UIKit 中的 View 或 ViewController 是長時間存在的UIView或UIViewController 類的實例芽腾,是對象。UIKit 中页衙,view 的創(chuàng)建和 view 的更新是兩條不同的代碼路徑摊滔。
SwiftUI 中的 View 是值,而非對象拷姿。SwiftUI 中沒有 Controller 的概念。View 是符合 View 協(xié)議的短時存在的值旱函。我們不必編寫多余的代碼來更新屏幕上的文本標簽响巢。每當狀態(tài)改變時,view 樹都會被重建棒妨。
二踪古、View的創(chuàng)建
要在 SwiftUI 中創(chuàng)建 view含长,你需要創(chuàng)建一棵包含 view 的值的樹,來描述應(yīng)該在屏幕上顯示的內(nèi)容伏穆。要更改屏幕上的內(nèi)容拘泞,你可以修改用 @State 修飾的值,這樣新的 view 值的樹會被重新計算枕扫。然后陪腌, SwiftUI 會更新屏幕,以反映這些新的 view 值烟瞧。
import SwiftUI
struct Me: View {
@State private var counter = 0
var body: some View {
VStack {
Button(action: { counter += 1 }, label: {
Text("Tap me!")
.padding()
.background(Color(.tertiarySystemFill))
.cornerRadius(5)
})
if counter > 0 {
Text("You've tapped \(counter) times")
} else {
Text("You've not yet tapped")
}
}
}
}
1诗鸭、View樹中可包含switch 和 if 語句,不能使用循環(huán)和guard
SwiftUI 利用了稱為函數(shù)構(gòu)建器 (function builder) 的 Swift 特性参滴。舉個例子强岸, VStack 之后的尾隨閉包并不是一個普通的 Swift 函數(shù);它是一個 ViewBuilder (它是由 Swift 的 函數(shù)構(gòu)建器特性實現(xiàn)的)。在 view 的構(gòu)建閉包中砾赔,你只能使用 Swift 的一個有限的子集來編寫 程序:例如蝌箍,你不能使用循環(huán)和 guard。但是暴心,你可以像上面示例中的 counter 變量一樣妓盲,編寫 switch 和 if 語句來構(gòu)造出依賴于 app 當前狀態(tài)的 view 樹。除了布爾值 if 語句以外酷勺,你可以使用的還有 if let本橙,if case let。
View 的樹不僅只包含當前可見的部分脆诉,它包含的是整個結(jié)構(gòu)甚亭,這是有優(yōu)點的:SwiftUI 能夠更有效地找出 view 更新后發(fā)生了什么變化。
2击胜、ModifiedContent 值(修飾器)的深層嵌套
我們在按鈕上使用的 padding亏狰、background 和 cornerRadius API 并不是簡單地去更改按鈕的屬性。實際上偶摔,這些 方法 (我們通常稱其為 “修飾器”) 的調(diào)用都會在 view 樹中創(chuàng)建新的一層暇唾。在按鈕上調(diào)用 .padding() 會將按鈕包裝為 ModifiedContent 類型的值,這個值中包含有關(guān)應(yīng)該如何設(shè)置 padding 填充的信息辰斋。在該值上再調(diào)用 .background策州,又會把現(xiàn)有值包裝起來,創(chuàng)建另一個 ModifiedContent 值宫仗,這一次將添加有關(guān)背景色的信息够挂。
3、順序通常很重要
調(diào)用 .padding().background(...) 與調(diào)用 .background(...).padding() 是不一樣的藕夫。在前一種情況下孽糖, 背景將延伸到填充部分的外邊緣;而在后一種情況下枯冈,背景只會出現(xiàn)在填充范圍的內(nèi)側(cè)。
4办悟、.border 調(diào)用在垂直堆棧周圍添加了 overlay 的修飾器尘奏,該修飾符使用其子元素的大小。
在 SwiftUI 中病蛉,你永遠不會強迫 view 直接使用一個特定的大小炫加。你只能將其包裝在 frame 修飾器中,它的可用空間將被提供給子元素铡恕。view 可以 定義自己的理想大小 (類似于 UIKit 的 sizeThatFits 方法)琢感,你可以強制讓 view 變成它們的理想 大小。
5探熔、更改狀態(tài)屬性是在 SwiftUI 中觸發(fā) view 更新的唯一方法驹针。
點擊按鈕會修改 @State counter 屬性,這會觸發(fā)這種更新 view 的狀態(tài)更改诀艰。觸發(fā) view 更新的屬性會被用 @State柬甥、@ObservedObject 或者 @StateObject 屬性標簽 進行標記。
我們不能直接更新屏幕上的內(nèi)容其垄。相反苛蒲,我們必須修改狀態(tài)屬性 (比如 @State 或 @ObservedObject),然后讓 SwiftUI 去找出 view 樹的變化方式绿满。
三臂外、View的更新
在大多數(shù)面向?qū)ο蟮?GUI 程序,有兩條與 view 相關(guān)的代碼路徑: 一條路徑處理 view 的初始構(gòu)造喇颁,另 一條路徑負責在事件發(fā)生時更新 view漏健。由于這些代碼路徑是分離開的,而且涉及手動更新橘霎,所 以很容易出現(xiàn)錯誤:我們可能會響應(yīng)事件來更新 view蔫浆,但卻忘了更新 model,反之亦有可能姐叁。 無論哪種情況瓦盛,view 都會與 model 不同步,app 可能會表現(xiàn)出不確定的行為外潜、卡死甚至崩潰原环。
AppKit 里使用 Cocoa Binding 技術(shù),它是一個可以使 model 和 view 保持同步的雙向?qū)哟T?UIKit 里嘱吗,人們使用像是響應(yīng)式編 程這樣的技術(shù)來讓這兩個代碼路徑 (在大部分情況下) 得到統(tǒng)一。
SwiftUI 的設(shè)計完全避免了此類問題碧库。首先柜与,只有 view 的 body 屬性這一個代碼路徑可以構(gòu)造 初始的 view,而且這條路徑也會用于所有的后續(xù)更新嵌灰。其次弄匕,SwiftUI 讓使用者無法繞過正常 的 view 的更新周期,也無法直接修改 view 樹沽瞭。在 SwiftUI 中迁匠,想要更新屏幕上的內(nèi)容,觸發(fā) 對 body 屬性的重新求值是唯一的方法驹溃。
SwiftUI 只會重新去執(zhí)行那些使用了 @State 屬性的 view 的 body 城丧。(對于其他屬性包裝,例如 @ObservedObject 和 @Environment豌鹤,也是一樣的)亡哄。
struct BindingView : View {
@Binding var counter: Int
var body: some View {
Button(action: { counter += 1 }, label: {
Text("Tap me!")
.padding()
.background(Color(.tertiarySystemFill))
.cornerRadius(5)
})
}
}
本質(zhì)上來說,binding 是它所捕獲變量的 setter 和 getter布疙。SwiftUI 的屬性包裝 (比如 @State蚊惯, @ObservedObject 等) 都有對應(yīng)的 binding,你可以在屬性名前加上 $ 前綴來訪問它灵临。(在屬性 包裝的術(shù)語中截型,binding 被叫做一個投射值 (projected value))。
四儒溉、屬性包裝
1宦焦、操作值類型
當數(shù)據(jù)是一個值類型的時候 (比如 struct,enum 或者是一個不可變對象)顿涣,我們有三種選擇: 使用普通的屬性波闹,使用 @State 屬性,或者使用 @Binding 屬性园骆。
2舔痪、操作對象
當你的數(shù)據(jù)是一個對象時,你可以讓它滿足 ObservableObject锌唾,這樣 SwiftUI 就能夠訂閱它的 變更锄码。對于可觀察的對象,有三個屬性包裝與它對應(yīng):當指向?qū)ο蟮囊每梢园l(fā)生變化時晌涕,使用 @ObservedObject;當引用不能改變時滋捶,使用 @StateObject;當對象是通過環(huán)境進行傳遞時, 使用 @EnvironmentObject余黎。
3重窟、ObservedObject
ObservableObject 協(xié)議的唯一要求是實現(xiàn) objectWillChange,它是一個 publisher惧财,會在對象 變更時發(fā)送事件巡扇。通過在 name 和 city 屬性前面添加 @Published扭仁,框架會為我們創(chuàng)建一個 objectWillChange 的實現(xiàn),在每次這兩個屬性發(fā)生改變的時候發(fā)送事件厅翔。
class Model: ObservableObject {
init() { print("Model Created") }
@Published var score: Int = 0
}
五乖坠、環(huán)境
環(huán)境 (environment) 是幫助我們理解 SwiftUI 工作方式的一塊重要拼圖。簡而言之刀闷,環(huán)境是 SwiftUI 用于將值沿 view 樹向下傳遞的機制熊泵。也就是說,值從父 view 傳遞到其包含的子 view 樹甸昏,是依靠環(huán)境完成的顽分。
1、環(huán)境是如何工作的
var body: some View {
VStack {
Text("Hello World!")
}
.font(Font.headline)
.debug()
}
/* ModifiedContent<
VStack<Text>,
_EnvironmentKeyWritingModifier<Optional<Font>> >
*/
這個類型告訴了我們施蜜,.font 調(diào)用將會把 VStack 包裝到另一個叫做 ModifiedContent 的 view 中卒蘸。這個 view 包含有兩個泛型參數(shù):第一個參數(shù)是內(nèi)容本身的類型,第二個是將被應(yīng)用到這個 內(nèi)容上的修飾器翻默。在本例中悬秉,第二個參數(shù)是私有的 _EnvironmentKeyWritingModifier,正如其 名冰蘑,它負責將一個值寫入到環(huán)境中和泌。對于 .font 調(diào)用來說,一個可選的 Font 值會被寫入到環(huán)境祠肥。 因為環(huán)境會依據(jù) view 樹向下傳遞武氓,所以 stack 中的文本標簽可以從環(huán)境中讀取這個字體。
2仇箱、自定義環(huán)境值
首先需要定義一個新的類型县恕,讓它遵守 EnvironmentKey 協(xié)議。EnvironmentKey 協(xié)議的唯一要求是一個靜態(tài)的 defaultValue 屬性剂桥。
因為 .environment API 通過從 EnvironmentValues 的鍵路徑來獲取對應(yīng)類型的值忠烛,所我們還要為 EnvironmentValues 添加一個屬性,這樣我們才能將它用作鍵路徑权逗。
最后去實現(xiàn)這個方法美尸。
private struct MyEnvironmentKey: EnvironmentKey {
static let defaultValue: String = "Default value"
}
extension EnvironmentValues {
var myCustomValue: String {
get { self[MyEnvironmentKey.self] }
set { self[MyEnvironmentKey.self] = newValue }
}
}
extension View {
func myCustomValue(_ myCustomValue: String) -> some View {
environment(\.myCustomValue, myCustomValue)
}
}
3、依賴注入
我們可以把環(huán)境看作是一種依賴注入;設(shè)置環(huán)境值等同于注入依賴斟薇,而讀取環(huán)境值則等同于接收依賴师坎。
不過,環(huán)境中通常使用的都是值類型:一個通過 @Environment 屬性依賴某個環(huán)境值的 view堪滨, 只會在一個新的環(huán)境值被設(shè)置到相應(yīng)的 key 時才會失效并重繪胯陋。如果我們在環(huán)境中存儲的是一個對象,并通過 @Environment 觀察它,view 并不會由于對象中的一個屬性變化而重繪遏乔,重繪 只在將 key 設(shè)置為整個不同的對象時才會發(fā)生义矛。然而,當我們在使用對象作為依賴時盟萨,完整的 對象替換往往不是我們期望的行為症革。
4、Preferences
環(huán)境允許我們將值從一個父 view 隱式地傳遞給它的子 view鸯旁,而 preference 系統(tǒng)則允許我們將值隱式地從子 view 傳遞給它們的父 view。
我們看到過像是 .font 和 .foregroundColor 這樣的修飾器量蕊,它們會改變各自的 view 子樹的環(huán)境铺罢。不過,.navigationBarTitle 要做的事情恰好相反:Text 并不關(guān)心標題残炮, 不過它的父 view 對此關(guān)心韭赘,然而,NavigationView 有可能不是它的直接父 view势就。
最后泉瞻,我們需要在我們的 MyNavigationView 中讀取 preference。要使用這個值苞冯,我們需要將它存儲在 @State 變量中袖牙。