SwiftUI view 的生命週期影響 StateObject & State property 儲存的資料

開發 SwiftUI App,為了實現 source of truth,確保單一資料來源,避免資料不同步的問題,我們會使用 State & Binding 管理 struct 型別的資料,使用 StateObject &ObservedObject 管理 class 型別的資料。

用 @State 宣告的 property 將是資料來源,其它以 @Binding 宣告的 property 將參考它,存取同一份資料。

同樣的,用 @StateObject 宣告的 property 將是資料來源,其它以 @ObservedObject 宣告的 property 將參考它,存取同一份資料。

不過到底 StateObject & ObservedObject 有什麼差別呢 ? StateObject & State 儲存的資料什麼時候會被清掉呢 ? 接下來讓我們解開其中的奧秘。

定義被觀察的資料型別 DiceObject

class DiceObject: ObservableObject {
@Published var value = 1

func roll() {
value = Int.random(in: 1...6)
}
}

當 view 重新產生時,ObservedObject 儲存的資料不會維持之前的內容

設定顯示骰子的 DiceView,點選 Play 會改變點數。

struct DiceView: View {
@ObservedObject var diceObject = DiceObject()

var body: some View {
VStack {
Image(systemName: "die.face.\(diceObject.value).fill")
.resizable()
.scaledToFit()
Button("Play") {
diceObject.roll()

}
}
}
}

在 GameView 裡顯示 DiceView,點選 Random Background Color 會改變背景顏色。


struct GameView: View {
@State private var color = Color.white

var body: some View {
ZStack {
color
.ignoresSafeArea()
VStack {
DiceView()
Button("Random Background Color") {
color = Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1))
}
}
.font(.title)
.padding()
}


}
}

struct GameView_Previews: PreviewProvider {
static var previews: some View {
GameView()
}
}

執行 App 後,我們先點選骰子下的 Play 隨機骰子點數。

接著點選 Random Background Color,骰子點數自動變成 1,為什麼會這樣呢 ?

當 GamView 的 color 改變時,造成了以下一連串的連鎖反應。

  • GameView 的 body 程式重新執行。
  • 重新產生 DiceView。
  • DiceView 裡的 diceObject 重新產生,因此 diceObject.value 的內容是初始值 1。

從以上實驗,我們發現當 view 因為畫面更新重新產生時,ObservedObject 儲存的資料不會維持之前的內容。

State & StateObject property 儲存的資料將一直存在,直到 view 的生命結束

若我們想要 view 重新產生時,資料能持續存在不受影響,則須將它宣告為 State 或 StateObject,它們儲存的資料將存在別的地方,因此不會因為 view 重新生成而被清掉。當 view 被建立和出現後,State & StateObject 儲存的資料將一直存在,直到 view 的生命結束。

接下來我們將 DiceView 裡的 diceObject 改成 StateObject 試試。

struct DiceView: View {
@StateObject var diceObject = DiceObject()

var body: some View {
VStack {
Image(systemName: "die.face.\(diceObject.value).fill")
.resizable()
.scaledToFit()
Button("Play") {
diceObject.roll()

}
}
}
}

執行 App 後,我們先點選骰子下的 Play 隨機骰子點數,接著點選 Random Background Color,此時骰子點數不受影響,證明了 DiceView 重新生成時,diceObject 儲存的資料持續存在,沒有被清掉。

依據 Apple 在 WWDC Demystify SwiftUI 的說明,view 的生命週期跟 identity 有關,當 view 被建立和出現後,SwiftUI 會設定它的 identity。只要 identity 沒變,就是同一個 view。

在剛剛的例子,GameView 的畫面更新時重新生成 DiceView,不過 DiceView 的 identity 並沒有變,因此還是算同一個 DiceView,DiceView 裡 StateObject property 儲存的資料不受影響。

view 只有在以下兩種情況會結束生命:

1. 從畫面上移除。

2. identity 改變。

接下來我們分別針對這兩種情況舉例說明。

當 view 從畫面上移除時,view 的生命結束,State & StateObject 儲存的資料將被清掉

依據 showDiceView 決定是否顯示 DiceView,當 showDiceView 為 false 時,DiceView 將從畫面移除。

struct GameView: View {
@State private var color = Color.white
@State private var showDice = true

var body: some View {
ZStack {
color
.ignoresSafeArea()
VStack {
if showDice {
DiceView()
}
Button("Random Background Color") {
color = Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1))
}
Button("\(showDice ? "hide" : "show") dice") {
showDice.toggle()
}
}
.font(.title)
.padding()
}
}
}

執行 App 後,進行以下操作。

  • 點選 Play,然後點選 Random Background Color。
  • 點選 hide dice。

此時 showDice 變成 false,因此 DiceView 被移除了,DiceView 的生命到此為止,diceObject 儲存的資料也被清除。

  • 點選 show dice。

此時 showDice 變成 true,因此顯示 DiceView。現在顯示的 DiceView 是全新的 DiceView,所以產生全新的 diceObject,存著初始值 1。

當 view 的 id 改變時,view 的生命結束,State & StateObject 儲存的資料將被清掉

當 view 的 identity 改變時,SwiftUI 會產生全新的 view,原本的 view 將被清掉,因此 State & StateObject 儲存的資料也會被清掉。

以下例子裡,點選 change dice ID 改變 diceID,而 DiceView 的 id 是 diceID,因此 DiceView 的 identity 改變了,產生全新的 DiceView。

struct GameView: View {
@State private var color = Color.white
@State private var diceID = UUID()

var body: some View {
ZStack {
color
.ignoresSafeArea()
VStack {
DiceView()
.id(diceID)
Button("Random Background Color") {
color = Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1))
}
Button("change dice ID") {
diceID = UUID()
}
}
.font(.title)
.padding()
}
}
}

執行 App 後,進行以下操作。

  • 點選 Play,然後點選 Random Background Color。
  • 點選 change dice ID。

DiceView 的 id 改變,產生全新的 DiceView,所以生成新的 diceObject,存著初始值 1。原本的 DiceView 將結束生命,它的 diceObject 也被一併清除。

State 的例子

剛剛的例子都是以 StateObject 說明,不過 State 也有類似的概念,SwiftUI view 的生命週期將影響 StateObject & State 儲存的資料。有興趣的朋友可參考以下 State 的範例說明。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com