SwiftUI:@State原理解析

@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)
  }
}
image1.png

nonmutating

到目前為止恕酸,我們的定義與在視圖本身中直接定義屬性沒有太大區(qū)別。
如果我們從ContentView聲明中刪除@FSState胯陋,一切仍然運(yùn)行良好:

struct ContentView: View {
  var text = "Hello Five Stars"

  var body: some View {
    Text(text)
  }
}
image1.png

讓我們現(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 immutabletext轉(zhuǎn)移到FSStatewrappedValue的setter方法中了缩滨。
這是有意義的势就,因?yàn)槲覀兂兄Z不改變struct實(shí)例,但我們?cè)O(shè)置value = newValue脉漏,這是可變的苞冯。

這就是Swift引用類型的由來:如果我們用class類型替換FSStatevalue屬性,然后在我們的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)
  }
}

更新后buildandrun我們的應(yīng)用!

image2.gif

我們點(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來匹配BoxObservableObject。組合關(guān)聯(lián)一個(gè)objectWillChangepublisher到所有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)用程序:

image2.gif

不幸的是寺庄,我們還沒到那一步艾蓝。當(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)用程序:

image3.gif

終于可以了讲婚!盡管添加了與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和綁定了!

image4.gif

下面是最終的@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)大!也許我們還有很多沒考慮到的地方僧界。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜜氨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捎泻,更是在濱河造成了極大的恐慌飒炎,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笆豁,死亡現(xiàn)場(chǎng)離奇詭異郎汪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)闯狱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門煞赢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哄孤,你說我怎么就攤上這事照筑。” “怎么了瘦陈?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵凝危,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我晨逝,道長(zhǎng)蛾默,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任捉貌,我火速辦了婚禮支鸡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘趁窃。我一直安慰自己牧挣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布醒陆。 她就那樣靜靜地躺著瀑构,像睡著了一般。 火紅的嫁衣襯著肌膚如雪统求。 梳的紋絲不亂的頭發(fā)上检碗,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音码邻,去河邊找鬼折剃。 笑死,一個(gè)胖子當(dāng)著我的面吹牛像屋,可吹牛的內(nèi)容都是我干的怕犁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼己莺,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼奏甫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起凌受,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤阵子,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后胜蛉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挠进,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年誊册,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了领突。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡案怯,死狀恐怖君旦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嘲碱,我是刑警寧澤金砍,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站麦锯,受9級(jí)特大地震影響捞魁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜离咐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一谱俭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宵蛀,春花似錦昆著、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至梧宫,卻和暖如春接谨,著一層夾襖步出監(jiān)牢的瞬間摆碉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工脓豪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留巷帝,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓扫夜,卻偏偏與公主長(zhǎng)得像楞泼,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子笤闯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容