昨天我們學會了 @State 與 @Binding:
這兩個工具對於小型 App 已經很好用了,但隨著 App 越來越複雜,我們可能會遇到以下情境:
這時候,就要用到 @ObservedObject、@StateObject 與 @EnvironmentObject。
ObservableObject 協定@Published),View 會自動更新// 可被觀察的資料模型
class UserData: ObservableObject {
    @Published var score = 0
}
struct ObservedExampleView: View {
    @ObservedObject var userData = UserData()
    var body: some View {
        VStack {
            Text("目前分數:\(userData.score)")
                .font(.title)
            Button("加一") {
                userData.score += 1
            }
            .padding()
            // 傳給子 View
            ScoreDetailView(userData: userData)
        }
    }
}
struct ScoreDetailView: View {
    @ObservedObject var userData: UserData
    var body: some View {
        Text("子畫面分數:\(userData.score)")
            .foregroundColor(.blue)
    }
}

如果物件不是由外部傳入,而是 直接在子 View 裡初始化,就會出問題:
struct BadExampleView: View {
    @State var isShow = true
    var body: some View {
        VStack {
            Text("Current User: \(isShow ? "A狀態" : "B狀態")")
            Button("父畫面切換換狀態") {
                isShow.toggle()
            }
            BadScoreDetailView()
        }
    }
}
struct BadScoreDetailView: View {
    @ObservedObject var userData: UserData = UserData()
    var body: some View {
        VStack {
            Text("錯誤子畫面分數:\(userData.score)")
                .foregroundColor(.blue)
            Button("加一") {
                userData.score += 1
            }
        }
    }
}
當父畫面刷新時,子畫面的 userData 會 被重置,導致資料消失。

點擊父畫面後資料消失。

為了解決這個問題,SwiftUI 提供了 @StateObject。
@ObservedObject 不同,ObservedObject 通常是 別人建立好傳進來的
在剛剛的範例中加上,再看一次結果
struct GoogScoreDetailView: View {
    @StateObject var userData: UserData = UserData()
    var body: some View {
        VStack {
            Text("正確的子畫面分數:\(userData.score)")
                .foregroundColor(.blue)
            Button("加一") {
                userData.score += 1
            }
        }
    }
}

可以看到子畫面沒有被刷新。

當 App 的規模更大,我們可能不希望一層一層傳資料。這時候就能使用**@EnvironmentObject**,把資料「注入環境」,讓所有需要的子 View 都能直接存取。
ObservableObject
// 共享的資料模型
class AppSettings: ObservableObject {
    @Published var isDarkMode = false
}
struct EnvironmentRootView: View {
    @StateObject var settings = AppSettings()  // 建立唯一資料來源
    var body: some View {
        VStack {
            Toggle("深色模式", isOn: $settings.isDarkMode)
                .padding()
            ChildAView()  // 不用手動傳,直接共享
            ChildBView()
        }
        .environmentObject(settings)  // 注入環境
    }
}
struct ChildAView: View {
    @EnvironmentObject var settings: AppSettings
    var body: some View {
        Text(settings.isDarkMode ? "深色模式 ON" : "深色模式 OFF")
            .foregroundColor(.green)
    }
}
struct ChildBView: View {
    @EnvironmentObject var settings: AppSettings
    var body: some View {
        Button("切換模式") {
            settings.isDarkMode.toggle()
        }
    }
}

父畫面注入一次 .environmentObject(settings),子畫面自動共享。
明天我們會回到 Layout,介紹 ScrollView 以及 Lazy 容器。