SwiftUI Property Wrappers 總整理
SwiftUI 是一個「狀態驅動 (State-driven)」的框架。我們不直接修改 View,而是修改 Data (狀態),然後 SwiftUI 會自動更新 View。
為了管理這些狀態,SwiftUI 提供了多種 Property Wrappers (屬性包裝器),例如 @State、@Binding、@StateObject 等。
很多初學者容易搞混:「什麼時候該用 @ObservedObject?什麼時候用 @StateObject?」
這篇文章將一次解析所有常用的 Property Wrappers。
快速對照表 (Cheat Sheet)
| Property Wrapper | 類型 | 擁有權 (Ownership) | 用途 |
|---|---|---|---|
@State | Value Type (Int, String...) | View 擁有 (Source of Truth) | View 內部的私有狀態,如 Toggle 開關、輸入框文字 |
@Binding | Value Type | 無 (引用自父層) | 讓子元件可以讀寫父元件的 @State |
@StateObject | Reference Type (Class) | View 擁有 (Source of Truth) | 建立並管理一個 ObservableObject 物件的生命週期 |
@ObservedObject | Reference Type (Class) | 無 (依賴注入) | 觀察外部傳入的物件,當物件變更時更新 View |
@EnvironmentObject | Reference Type (Class) | 無 (全局共享) | 跨越多層 View 共享資料,類似全域變數 |
@AppStorage | Value Type | System 擁有 | 自動讀寫 UserDefaults |
數值型狀態 (@State & @Binding)
這是最基礎的組合,通常用於處理簡單的資料 (Int, Bool, String, Struct)。重點在於理解 「父視圖擁有資料,子視圖引用資料」 的關係。
場景範例:開關控制
假設我們有一個父視圖 (Parent) 控制燈光,並且拆分出一個子視圖 (ToggleSwitch) 來負責顯示開關。
Step 1: 父視圖宣告 @State
@State 是 Source of Truth (資料的源頭)。父視圖擁有這個變數,並負責初始化它。
struct LightRoomView: View {
// 1. 父視圖擁有狀態,並初始化為 false
@State private var isLightOn = false
var body: some View {
VStack {
// 根據狀態顯示不同背景色
Color(isLightOn ? .yellow : .black)
// 2. 將狀態傳遞給子視圖
// 使用 $ 符號來建立 Binding (投影)
LightSwitch(isOn: $isLightOn)
}
}
}
Step 2: 子視圖宣告 @Binding
子視圖不擁有資料,它只是透過 @Binding 來「引用」父視圖的資料。
struct LightSwitch: View {
// 3. 子視圖宣告 Binding,注意這裡「沒有」初始化值
// 因為它的值是來自外部的
@Binding var isOn: Bool
var body: some View {
// 4. 當我們在子視圖修改 isOn 時...
Toggle("Switch", isOn: $isOn)
.padding()
// ...父視圖的 isLightOn 也會同步變更!
}
}
關鍵點:
- 父視圖用
@State private var value = ...(擁有資料)。 - 傳遞時用
$value(傳遞綁定)。 - 子視圖用
@Binding var value: Type(接收綁定,不可有預設值)。
補充:為什麼是加上「$」字號?
你可能會好奇,為什麼傳遞 Binding 時要用 $isLightOn 而不是 isLightOn?
這是 Swift Property Wrapper 的特殊語法。
isLightOn:存取的是 Wrapped Value (被包裝的值),也就是Bool本身 (true/false)。$isLightOn:存取的是 Projected Value (投射值)。對於@State來說,它的投射值就是一個 Binding (綁定)。
所以在 LightRoomView 中,我們需要傳遞「綁定」給子視圖,因此使用 $isLightOn。
物件型狀態 (@StateObject & @ObservedObject)
當狀態邏輯較複雜,或是需要跨 View 共享時,我們會將邏輯封裝在 class 中,並遵循 ObservableObject 協定。
class UserViewModel: ObservableObject {
@Published var name = "Mike"
}
這兩者的差別是面試必考題,也是最大的地雷區。
@StateObject (物件擁有者)
負責「建立」物件。SwiftUI 會確保在 View 重新渲染時,這個物件不會被銷毀重建。這是 Source of Truth。
struct ParentView: View {
// 這裡使用 @StateObject,因為是這個 View 負責建立它
@StateObject private var viewModel = UserViewModel()
var body: some View {
ChildView(viewModel: viewModel)
}
}
@ObservedObject (物件觀察者)
只負責「觀察」。它不保證物件的生命週期。如果錯用成 @ObservedObject 來建立物件,當 View 重繪時,資料可能會被重置。
struct ChildView: View {
// 這裡使用 @ObservedObject,因為物件是從外面傳進來的
@ObservedObject var viewModel: UserViewModel
var body: some View {
Text(viewModel.name)
}
}
A = B() ) 使用 @StateObject。
其他所有傳遞引用的地方都使用 @ObservedObject。環境與全局 (@EnvironmentObject)
如果你有一個資料需要讓整個 App 的幾十個 View 都能存取 (例如:登入的 User 資料、App 主題設定),一層一層傳遞太痛苦了。
這時可以使用 @EnvironmentObject,它像是一個「依賴注入 (Dependency Injection)」系統。
注入 (Inject)
在最頂層 (通常是 App 入口或某個容器 View) 使用 .environmentObject() modifier。
@main
struct MyApp: App {
@StateObject var userSettings = UserSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userSettings) // 注入
}
}
}
讀取 (Read)
在樹狀結構下的任何子 View,只要宣告就能直接拿來用。
struct DeepChildView: View {
// 自動尋找祖先層的 UserSettings 實例
@EnvironmentObject var settings: UserSettings
var body: some View {
Text(settings.username)
}
}
系統環境變數 (@Environment)
SwiftUI 內建了許多環境變數,例如:亮暗色模式、系統語系、EditMode 等。
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss // iOS 15+ 用來關閉頁面
var body: some View {
if colorScheme == .dark {
Text("Dark Mode")
}
Button("Close") {
dismiss()
}
}
}
持久化儲存 (@AppStorage)
它是 UserDefaults 的包裝器。使用起來就像 @State 一樣簡單,但資料會自動寫入 UserDefaults,下次開啟 App 時資料還在。
struct SettingsView: View {
// 自動讀取和寫入 key 為 "username" 的 UserDefaults
@AppStorage("username") var username: String = "Guest"
var body: some View {
TextField("Name", text: $username)
}
}
總結:我該用哪個?
- 簡單變數 (Int, String) → 用
@State。 - 需要傳給子層修改 → 子層用
@Binding。 - 複雜邏輯物件 (Class):
- 如果是 我在這裡初始化它 → 用
@StateObject。 - 如果是 別人傳給我的 → 用
@ObservedObject。
- 如果是 我在這裡初始化它 → 用
- 全 App 共用的資料 → 用
@EnvironmentObject。 - 需要存到 UserDefaults → 用
@AppStorage。