iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Mobile Development

從零開始學習 iOS系列 第 13

從零開始學習 iOS Day12 - 狀態管理 II

  • 分享至 

  • xImage
  •  

昨天我們學會了 @State@Binding

  • @State:單一 View 自己管理狀態
  • @Binding:子 View 可以修改父 View 的狀態

這兩個工具對於小型 App 已經很好用了,但隨著 App 越來越複雜,我們可能會遇到以下情境:

  • 需要 跨多個畫面 共用資料
  • 某些資料要被 多個元件同時監聽
  • 避免狀態在畫面刷新後被「重置」

這時候,就要用到 @ObservedObject@StateObject@EnvironmentObject

@ObservedObject

  • 適合:某個「資料物件」需要被多個 View 監聽與更新
  • 特點:
    • 資料物件必須遵守 ObservableObject 協定
    • 當資料內的屬性改變時(需要標記 @Published),View 會自動更新
    • 必須手動將物件傳入子 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)
    }
}

https://github.com/jian-fu-hung/ithelp-2025/blob/main/image/Day12/%E6%88%AA%E5%9C%96%202025-09-26%20%E5%87%8C%E6%99%A812.31.16.png?raw=true

ObservedObject 的限制

如果物件不是由外部傳入,而是 直接在子 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被重置,導致資料消失。

https://github.com/jian-fu-hung/ithelp-2025/blob/main/image/Day12/%E6%88%AA%E5%9C%96%202025-09-26%20%E5%87%8C%E6%99%A812.36.54.png?raw=true

點擊父畫面後資料消失。

https://github.com/jian-fu-hung/ithelp-2025/blob/main/image/Day12/%E6%88%AA%E5%9C%96%202025-09-26%20%E5%87%8C%E6%99%A812.37.30.png?raw=true

為了解決這個問題,SwiftUI 提供了 @StateObject

@StateObject

  • 適合:某個 ObservableObject 物件應該由這個 View 自己「建立並持有」。
  • 特點:
    • 資料物件只會在 View 初始化時建立一次
    • 即使 View 重新繪製(re-render),資料也不會被重置
    • @ObservedObject 不同,ObservedObject 通常是 別人建立好傳進來的

在剛剛的範例中加上,再看一次結果

struct GoogScoreDetailView: View {
    @StateObject var userData: UserData = UserData()

    var body: some View {
        VStack {
            Text("正確的子畫面分數:\(userData.score)")
                .foregroundColor(.blue)
            Button("加一") {
                userData.score += 1
            }
        }

    }
}

https://github.com/jian-fu-hung/ithelp-2025/blob/main/image/Day12/%E6%88%AA%E5%9C%96%202025-09-26%20%E5%87%8C%E6%99%A812.40.26.png?raw=true

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

https://github.com/jian-fu-hung/ithelp-2025/blob/main/image/Day12/%E6%88%AA%E5%9C%96%202025-09-26%20%E5%87%8C%E6%99%A812.40.34.png?raw=true

@EnvironmentObject

當 App 的規模更大,我們可能不希望一層一層傳資料。這時候就能使用**@EnvironmentObject**,把資料「注入環境」,讓所有需要的子 View 都能直接存取。

  • 跨多個畫面、甚至不在同一層級的 View 共享資料
  • 特點:
    • 一樣要用 ObservableObject
    • 可以直接「注入環境」,不需要一層一層手動傳
    • 常見於大型 App 的「全域狀態」管理
// 共享的資料模型
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()
        }
    }
}

https://github.com/jian-fu-hung/ithelp-2025/blob/main/image/Day12/%E6%88%AA%E5%9C%96%202025-09-26%20%E5%87%8C%E6%99%A812.43.12.png?raw=true

父畫面注入一次 .environmentObject(settings),子畫面自動共享

今日小結

  • @ObservedObject:多個 View 可觀察、共享同一個資料,但需要手動傳遞。
  • @StateObject:由自己建立並持有的資料來源,避免被重置。
  • @EnvironmentObject:直接放到環境裡,所有需要的 View 都能存取,適合大型 App。

明天我們會回到 Layout,介紹 ScrollView 以及 Lazy 容器。


上一篇
從零開始學習 iOS Day11 - 狀態管理 I
系列文
從零開始學習 iOS13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言