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)用途
@StateValue Type (Int, String...)View 擁有 (Source of Truth)View 內部的私有狀態,如 Toggle 開關、輸入框文字
@BindingValue Type (引用自父層)讓子元件可以讀寫父元件的 @State
@StateObjectReference Type (Class)View 擁有 (Source of Truth)建立並管理一個 ObservableObject 物件的生命週期
@ObservedObjectReference Type (Class) (依賴注入)觀察外部傳入的物件,當物件變更時更新 View
@EnvironmentObjectReference Type (Class) (全局共享)跨越多層 View 共享資料,類似全域變數
@AppStorageValue TypeSystem 擁有自動讀寫 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)
    }
}
如果忘記在祖先層注入而在子層讀取,App 會直接 Crash。

系統環境變數 (@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)
    }
}

總結:我該用哪個?

  1. 簡單變數 (Int, String) → 用 @State
  2. 需要傳給子層修改 → 子層用 @Binding
  3. 複雜邏輯物件 (Class)
    • 如果是 我在這裡初始化它 → 用 @StateObject
    • 如果是 別人傳給我的 → 用 @ObservedObject
  4. 全 App 共用的資料 → 用 @EnvironmentObject
  5. 需要存到 UserDefaults → 用 @AppStorage