SwiftUI 觀察物件 @StateObject @ObservedObject

在 MVVM 模式中,我們使用 ViewModel 類別來管理狀態。在 View 中引用這個 ViewModel 時,有兩個非常相似的屬性包裝器:@StateObject@ObservedObject。搞混它們是初學者最常見的錯誤之一。

核心區別

  • @StateObject: 負責建立持有物件的生命週期。即使 View 被重新繪製 (Redraw),物件不會被銷毀重建。
  • @ObservedObject: 僅負責觀察已存在的物件。如果 View 被重新繪製,@ObservedObject 可能會導致資料遺失(如果它不小心被重新初始化的話)。

何時使用哪個?

1. View 擁有 ViewModel (Source of Truth)

如果這個 View 是 ViewModel 的擁有者(也就是在這個 View 裡初始化 ViewModel),請務必使用 @StateObject

struct PostListView: View {
    // 正確:這個 View 負責建立 VM,所以用 StateObject
    @StateObject private var viewModel = PostViewModel() 
    
    var body: some View { ... }
}

2. View 接收外部傳來的 ViewModel

如果 ViewModel 是由父視圖建立好並傳進來的,這個 View 只是借用並觀察它,請使用 @ObservedObject

struct PostRowView: View {
    // 正確:VM 是從外面傳進來的,我只是觀察它
    @ObservedObject var viewModel: PostViewModel 
    
    var body: some View { ... }
}

實驗:生命週期陷阱

口說無憑,我們來做個實驗。

我們定義一個會隨機產生數字的 ViewModel:

class RandomModel: ObservableObject {
    @Published var number: Int = Int.random(in: 1...1000)
}

然後建立一個 View,裡面分別用兩種方式宣告:

struct RandomNumberView: View {
    // 正確:View 重繪時,它會保持不變
    @StateObject var stateVM = RandomModel()
    
    // 錯誤:View 重繪時,它會被「重置」
    @ObservedObject var observedVM = RandomModel()
    
    @State private var counter = 0
    
    var body: some View {
        VStack {
            Text("StateObject: \(stateVM.number)") // 數字保持不變
            Text("ObservedObject: \(observedVM.number)") // 每次按按鈕都會變!
            
            Button("Refresh View (\(counter))") {
                counter += 1 // 觸發 View 重繪
            }
        }
    }
}

結果分析

當你點擊 "Refresh View" 按鈕時,counter 改變,導致 View 需要重繪。

  1. RandomNumberView struct 被重新建立。
  2. stateVM 因為是 StateObject,SwiftUI 知道「我已經存過它了」,所以忽略新產生的 RandomModel(),繼續用舊的。
  3. observedVM 因為是 ObservedObject,它沒有記憶能力,所以它就直接使用了新產生的 RandomModel()。導致數字一直跳動。

這就是為什麼你不能在 View 內部用 @ObservedObject 來初始化物件!

常見錯誤

錯誤寫法

struct ContentView: View {
    @ObservedObject var viewModel = MyViewModel() // 錯誤!
}

如果你這樣寫,每次 ContentView 因為任何原因 (比如父層更新) 被重新初始化時,viewModel 也會被重新建立,導致之前的狀態 (如輸入到一半的文字) 全部重置。

口訣:誰生它,誰就用 StateObject;誰只是看它,誰就用 ObservedObject。