iT邦幫忙

2021 iThome 鐵人賽

DAY 10
0
Software Development

iOS 學習筆記系列 第 10

Day10 SwiftUI 03 - SwiftUI 數據流

SwiftUI 數據流

SwiftUI 作為一個聲明式的 UI 框架,幫我們處理了幾乎所有關於 介面UI數據 之間的交互,通過數據流向對視圖更新等操作,應用程序運行時,關於介面的修改,只能透過修改數據來間接完成,而不是直接對介面面進行修改操作,SwiftUI 又以單一數據源(single source of truth)為核心,構建了數據驅動狀態更新的機制

而為了實現數據和UI 的綁定,需要透過不同的關鍵字屬性包裝器Property Wrapper 來向SwiftUI 描述它們之間的關係,用來進行狀態管理

  • Property

    一般在View Strcut 裡定義的varlet屬性,只能用來讀取

  • @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 {
      //深色模式畫面
    }
    

上一篇
Day09 SwiftUI 02 - 在 SwiftUI 上設計畫面
下一篇
Day11 SwiftUI 04 - 在SwiftUI 上設計多畫面
系列文
iOS 學習筆記30

尚未有邦友留言

立即登入留言