@State
是SwiftUI的眾多支柱之一霞丧,一旦理解了它蛀骇,我們就會(huì)理所當(dāng)然地認(rèn)為它無處不在,毫不猶豫地使用村缸。但是@State
是什么呢?幕后發(fā)生了什么?
在本文中,讓我們嘗試通過重建@State
等來回答這些問題武氓。
因?yàn)槲覠o法訪問實(shí)際的swift代碼/實(shí)現(xiàn)梯皿,我們將分析模仿原始@State行為
Property wrapper屬性包裝
首先,@State
是一個(gè)屬性包裝器县恕,簡(jiǎn)而言之东羹,它是一個(gè)具有額外邏輯和存儲(chǔ)的高級(jí)getter和setter。
讓我們先定義我們的狀態(tài)如下:
@propertyWrapper
struct FSState {
}
屬性包裝器需要一個(gè)wrappedValue
忠烛,讓我們可以讀/寫相關(guān)的值属提。
因?yàn)槲覀兿胍M@State
,所以我們將屬性包裝器泛型到類型V
上美尸,并將原始值存儲(chǔ)在內(nèi)部value
屬性中:
@propertyWrapper
struct FSState<V> {
// This is where our value is actually stored.
var value: V
// And here are our getter/setters.
var wrappedValue: V {
get {
value
}
set {
value = newValue
}
}
}
最后冤议,如果我們想提供與@State
和所有其他屬性包裝器相同的語法(例如,@State var x = "hello"@State var x = "hello"
),我們需要聲明一個(gè)特殊的初始化方法:
@propertyWrapper
struct FSState<V> {
var value: V
var wrappedValue: V {
...
}
init(wrappedValue value: V) {
self.value = value
}
}
有了這個(gè)定義,我們現(xiàn)在可以開始在視圖中使用@FSState
师坎,例如:
struct ContentView: View {
@FSState var text = "Hello Five Stars"
var body: some View {
Text(text)
}
}
nonmutating
到目前為止恕酸,我們的定義與在視圖本身中直接定義屬性沒有太大區(qū)別。
如果我們從ContentView
聲明中刪除@FSState
胯陋,一切仍然運(yùn)行良好:
struct ContentView: View {
var text = "Hello Five Stars"
var body: some View {
Text(text)
}
}
讓我們現(xiàn)在嘗試用一個(gè)按鈕來改變text文本蕊温,例如:
struct ContentView: View {
@FSState var text = "Hello Five Stars"
var body: some View {
VStack {
Text(text)
Button("Change text") {
text = ["hello", "five", "stars"].randomElement()!
}
}
}
}
不幸的是袱箱,這不會(huì)build成功:我們會(huì)得到一個(gè)按鈕操作錯(cuò)誤提示Cannot assign to property: 'self' is immutable
。問題是寿弱,分配的文本會(huì)改變ContentView
犯眠。
使用結(jié)構(gòu)體,我們可以聲明mutating
的方法症革,但不能聲明mutating
的計(jì)算屬性(如body)筐咧,也不能在其中調(diào)用mutating
的方法。
為了克服這個(gè)問題噪矛,我們不能改變ContentView
量蕊,這意味著我們也不能改變FSState
,因?yàn)槲覀兊膶傩园b器只是嵌套在視圖中的另一個(gè)值類型艇挨。
首先残炮,讓我們聲明我們的屬性包裝器設(shè)置為nonmutating
,它告訴Swift設(shè)置這個(gè)值不會(huì)改變我們的FSState
實(shí)例:
@propertyWrapper
struct FSState<V> {
var value: V
var wrappedValue: V {
get { ... }
nonmutating set { // our setter is now nonmutating
value = newValue
}
}
...
}
現(xiàn)在我們已經(jīng)將構(gòu)建錯(cuò)誤Cannot assign to property: 'self' is immutable
從text
轉(zhuǎn)移到FSState
的wrappedValue
的setter方法中了缩滨。
這是有意義的势就,因?yàn)槲覀兂兄Z不改變struct實(shí)例,但我們?cè)O(shè)置value = newValue
脉漏,這是可變的苞冯。
這就是Swift引用類型的由來:如果我們用class類型替換FSState
的value
屬性,然后在我們的setter方法中更新這個(gè)類實(shí)例侧巨,我們實(shí)際上并沒有更改FSState
(因?yàn)?code>FSState只包含對(duì)該類的引用舅锄,它總是保持不變)。
讓我們把"container"定義成class類型:
final class Box<V> {
var value: V
init(_ value: V) {
self.value = value
}
}
Box
是一個(gè)泛型類司忱,只有一個(gè)函數(shù):擁有和更新我們的值皇忿。
讓我們利用這個(gè)類給@FSState
聲明一個(gè)屬性:
@propertyWrapper
struct FSState<V> {
var box: Box<V>
var wrappedValue: V {
get {
box.value
}
nonmutating set {
box.value = newValue
}
}
init(wrappedValue value: V) {
self.box = Box(value)
}
}
更新后build
andrun
我們的應(yīng)用!
我們點(diǎn)擊按鈕坦仍,但沒有看到任何變化鳍烁,如果我們?cè)O(shè)置斷點(diǎn),我們將看到一切工作:點(diǎn)擊按鈕可以設(shè)置和更新我們的狀態(tài)繁扎,但是SwiftUI并不知道幔荒。
沒錯(cuò),我們更新數(shù)據(jù)锻离,但SwiftUI并不知道它應(yīng)該監(jiān)聽這些變化铺峭,并重新繪制body墓怀,讓我們接下來解決這個(gè)問題。
DynamicProperty
與SwiftUI中已知的基礎(chǔ)視圖類似,SwiftUI中每個(gè)視圖都可以根據(jù)視圖中定義的屬性監(jiān)聽這些publisher熬粗。
SwiftUI團(tuán)隊(duì)在隱藏SwiftUI大量使用Combine方面做了很多的工作:當(dāng)我們將一個(gè)視圖屬性與@State
布持、@ObservedObject
等關(guān)聯(lián)起來時(shí)莉炉,SwiftUI會(huì)監(jiān)聽連接到每個(gè)屬性包裝器的所有發(fā)布者,然后這些發(fā)布者會(huì)告訴SwiftUI什么時(shí)候重新繪制碴犬。
在我們的例子中絮宁,我們使用@StateObject
來匹配Box
的ObservableObject
。組合關(guān)聯(lián)一個(gè)objectWillChange
publisher到所有ObservableObject
實(shí)例服协,然后我們可以通過調(diào)用send()
將事件發(fā)送到SwiftUI:
final class Box<V>: ObservableObject {
var value: V {
willSet {
// This is where we send out our "hey, something has changed!" event
objectWillChange.send()
}
}
init(_ value: V) {
self.value = value
}
}
有更簡(jiǎn)單的方法來聲明它绍昂,但在本文中,我們?cè)噲D通過盡可能多地刪除“魔法”來了解事情是如何工作的偿荷。有更簡(jiǎn)單的方法來聲明它窘游,但在本文中,我們?cè)噲D通過盡可能多地刪除“魔法”來了解事情是如何工作的跳纳。
隨著Box
定義的更新忍饰,我們現(xiàn)在可以回到@FSState
,并將@StateObject
關(guān)聯(lián)到Box
屬性:
@propertyWrapper
struct FSState<V> {
@StateObject var box: Box<V>
var wrappedValue: V {
...
}
init(wrappedValue value: V) {
self._box = StateObject(wrappedValue: Box(value))
}
}
由于每次更新box的值變化:
-
objectWillChange
事件被觸發(fā) -
box
的publisher將會(huì)監(jiān)聽到
讓我們?cè)俅芜\(yùn)行我們的應(yīng)用程序:
不幸的是寺庄,我們還沒到那一步艾蓝。當(dāng)我們的值發(fā)生變化時(shí),新的發(fā)布者確實(shí)會(huì)發(fā)送事件斗塘,但是我們?nèi)匀恍枰嬖VSwiftUI:從SwiftUI的角度來看赢织,ContentView
有一個(gè)類型為FSState<String>
的text
屬性,這不是SwiftUI需要關(guān)注的逛拱。
要改變這一點(diǎn)敌厘,我們需要FSState
遵守DynamicProperty
協(xié)議,在文檔中描述為An interface for a stored variable that updates an external property of a view.
朽合。
這正是SwiftUI關(guān)注的!通過使FSState
遵守DynamicProperty
協(xié)議, SwiftUI將監(jiān)聽它的事件并在需要時(shí)觸發(fā)重繪俱两。
DynamicProperty
只需要一個(gè)update()
函數(shù)的實(shí)現(xiàn),然而SwiftUI已經(jīng)提供了它的默認(rèn)實(shí)現(xiàn)曹步,我們需要做的就是添加DynamicProperty
的一致性宪彩,然后就可以了:
@propertyWrapper
struct FSState<V>: DynamicProperty {
...
}
通過最后的修改,讓我們嘗試再次運(yùn)行我們的應(yīng)用程序:
終于可以了讲婚!盡管添加了與DynamicProperty
一致的屬性尿孔,我們?nèi)匀粵]有明確聲明SwiftUI應(yīng)該監(jiān)聽哪些屬性:與view Equatable的工作方式類似,我懷疑SwiftUI使用Swift的反射來迭代所有存儲(chǔ)的屬性筹麸,并尋找要訂閱的已知屬性包裝類型活合。
Binding
屬性包裝器的一個(gè)可選特性是公開一個(gè)投影值:投影值是存儲(chǔ)在屬性包裝器中的值的另一種查看方式,以不同的方式公開物赶。
許多SwiftUI視圖使用綁定來引用和潛在地改變其他地方擁有和存儲(chǔ)的值白指。一個(gè)例子是TextField
,它使用了一個(gè)Binding<String>
:
struct ContentView: View {
@FSState var text = ""
var body: some View {
VStack {
TextField("Write something", text: $text) // TextField's text is a binding
}
}
}
如上所述酵紫,我們可以通過在屬性名前加上$
來調(diào)用關(guān)聯(lián)屬性告嘲,從而從@State
獲得綁定错维,這個(gè)符號(hào)真正做的是獲取投影值而不是包裝的值。
因此@State
的投影值是@Binding
的一個(gè)V
類型的泛型值橄唬,讓我們?cè)?code>@FSState中添加相同的投影值:
@propertyWrapper
struct FSState<V>: DynamicProperty {
@ObservedObject private var box: Box<V>
var wrappedValue: V {
...
}
var projectedValue: Binding<V> {
Binding(
get: {
wrappedValue
},
set: {
wrappedValue = $0
}
)
}
...
}
瞧赋焕,我們現(xiàn)在可以使用@FSState
和綁定了!
下面是最終的@FSState
定義:
@propertyWrapper
struct FSState<V>: DynamicProperty {
@StateObject private var box: Box<V>
var wrappedValue: V {
get {
box.value
}
nonmutating set {
box.value = newValue
}
}
var projectedValue: Binding<V> {
Binding(
get: {
wrappedValue
},
set: {
wrappedValue = $0
}
)
}
init(wrappedValue value: V) {
self._box = StateObject(wrappedValue: Box(value))
}
}
final class Box<T>: ObservableObject {
var value: T {
willSet {
objectWillChange.send()
}
}
init(_ value: T) {
self.value = value
}
}
總結(jié)
我們對(duì)SwiftUI研究得越多,它就越能說明在一個(gè)簡(jiǎn)單仰楚、優(yōu)雅的API中隱藏著多少?gòu)?fù)雜性隆判。@FSState
不像真正的@State
那樣完整和強(qiáng)大!也許我們還有很多沒考慮到的地方僧界。