SwiftUI 作為一個聲明式的 UI 框架,幫我們處理了幾乎所有關於 介面UI 和 數據 之間的交互,通過數據流向對視圖更新等操作,應用程序運行時,關於介面的修改,只能透過修改數據來間接完成,而不是直接對介面面進行修改操作,SwiftUI 又以單一數據源(single source of truth)為核心,構建了數據驅動狀態更新的機制
而為了實現數據和UI 的綁定,需要透過不同的關鍵字屬性包裝器Property Wrapper 來向SwiftUI 描述它們之間的關係,用來進行狀態管理
Property
一般在View Strcut 裡定義的var、let屬性,只能用來讀取
@State 可變屬性(可觀察屬性)
Struct 結構裡的屬性不能直接修改,需把**@State關鍵字放置到屬性前修飾,該屬性實際上會被放到Struct 的外部儲存**起來,這意味著SwiftUI 能夠隨時銷毀和重建Struct 而不會丟失屬性的值
當被@State 包裝的屬性改變時,SwiftUI 會重新渲染使用到該屬性的視圖,系統會自動的重新計算View 的body 部分,構建出View Tree,由於View 都是Struct,SwiftUI 每次構建這個View Tree 都極快,這使得性能有很強的保障
視圖經常會被系統重建,所以需要給屬性賦一個預設值,若屬性被標記為@State,系統之後會使用儲存的變量的值,而不是每次都使用初始化的預設值
被@State包裝的屬性一定要用private修飾,並且這個變量只能在當前View 的body 內修改,所以它的使用場景是只影響當前View  內部的變化的操作
當一個屬性使用@State關鍵詞時,Swift 會為這個屬性添加一些額外的方法,比如Boolean 類型會有toggle() 方法,用於在true/false 之間切換
範例:
struct ContentView: View {
    @State private var isOn = false
    var body: some View {
        VStack {
            Text("Switch State: \(isOn ? "On" : "Off")")
            Button("change") {
                isOn.toggle()
            }
            .padding()
        }
    }
}
@ObservedObject
一般情況下數據來自本地Local 端或者遠端Remote API,這些數據預設與SwiftUI 沒有任何關係,而我們需要建立數據與介面的依賴關係就要使用到@ObservedObject和@Published,@ObservedObject允許外部進行使用和修改
@ObservedObject告訴SwiftUI,這個物件是可以被觀察的,裡面含有被@Published包裝了的屬性,而@ObservedObject包裝的物件必須是Class的物件,且必須實作ObservableObject,不能是Struct
@ObservedObject搭配著@Published一起使用,允許我們創建出能夠被自動觀察的物件屬性,SwiftUI 會自動監視觀察這個屬性,一旦發生改變,會自動修改與該屬性綁定的介面
範例:
建立一個ObservedObject 的類別
class Contact: ObservableObject {
    @Published var name: String
    @Published var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}
當被@Published包裝的變量改變時,body 會使用新值重新加載刷新頁面
struct ContentView: View {
    @ObservedObject var person = Contact(name: "Ryder", age: 27)
    var body: some View {
        VStack {
            Text("name:\(person.name)")
            Button("change") {
                person.name = "Ryder2"
            }
        }
    }
}
@StateObject
與@ObservedObject功能類似,區別在於當view 刷新時被@ObservedObject包裝的屬性會重置到初始值,而被StateObject使用的不會,就是說隨著View 的創建被多次創建(復用)@ObservedObject包裝的屬性將會多次創建(浪費資源),而@StateObject包裝的屬性只會被創建一次(節約資源),@StateObject基本上來說就是一個針對Class 的@State升級版,因此除非在某些必要的情況下需要使用ObservedObject 之外,大多數情況都適用於StateObject
@Binding 屬性綁定
聲明一個屬性是從外部獲取的,並且與外部是共享的,作用是在保存狀態的屬性和更改數據的視圖之間創建雙向連接,將當前屬性連接到儲存在別處的單一數據源(single source of truth),而不是直接儲存數據。將儲存在別處的值的屬性轉換為引用類型,在使用時需要在變量名前加$符號
通常使用場景是把當前View 中的@State值傳遞給其子View,要在子View 裡修改上層數據,需透過修改@Binding屬性觸發父視圖@State改變並重新計算body 後更新視圖,可以實現反向數據流的功能(雙向綁定),如果在子View 使用的是@State值,子視圖中的@State屬性會成為新的單一數據源,那麼子View 中對值的某個屬性進行修改,父View 就不會得到變化,所以需要在子視圖中使用@Binding
範例:
struct ContentView: View {
    @State private var isWifiOn = false
    var body: some View {
        VStack {
            Text("Wifi state: \(isWifiOn ? "On" : "Off")")
          	// 向下傳遞子視圖的binding參數 isOn,透過$符號傳遞引用 isWifiOn
            WifiView(isOn: $isWifiOn)
            .padding()
        }
    }
}
struct WifiView:View {
  	// 引用外部傳遞的參數
    @Binding var isOn:Bool
    var body: some View{
        VStack{
            Button("wifi change") {
              	// 切換時,由於Binding 機制的作用,會修改引用的外部單一數據源
                isOn.toggle()
            }
        }
    }
}
@EnvironmentObject
主要是為了在整個應用程序中共享物件,在View 物件裡通過environmentObject()方法向下傳遞全局數據,底下所有的視圖皆共享同一個環境物件並且可以監聽變更
範例:
// 設定要共享的數據類別
class DataSource: ObservableObject {
    @Published var counter = 0
}
struct ContentView: View {
    let dataSource = DataSource()
    var body: some View {
        VStack {
            Button("Click") {
                // 改變共享的數據,其他有用此數據的視圖也將重載
                dataSource.counter += 1
            }
            DisplayView()
        }
        // 通過在View 使用`environmentObject()`方法向下傳遞全局數據
        .environmentObject(dataSource)
    }
}
struct DisplayView: View {
    @EnvironmentObject var dataSource: DataSource
    var body: some View {
        Text("Click times:\(dataSource.counter)")
    }
}
@Environment 系統環境數據
與@EnvironmentObject作用不同,@Environment是從不同硬體設置及軟體環境中取出預設定義的值(系統內建屬性),比如獲得當前是深色模式還是正常模式,螢幕的大小等等
範例:
@Environment(\.colorScheme) var colorScheme: ColorScheme
if colorScheme == .light {
  //正常模式畫面  
} else {
  //深色模式畫面
}