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 的範例說明。