SwiftUI 中一些和響應式狀態有關的屬性包裝器的用途

SwiftUI 借鑑了 React 等 UI 框架的概念,經過 state 的變化,對 View 進行響應式的渲染。主要經過 @State, @StateObject, @ObservedObject 和 @EnvironmentObject 等屬性包裝器 (property wrapper) 將屬性包裝成狀態來實現。html

@State 和 @StateObject

@State 和 @StateObject 是比較經常使用的屬性包裝器。git

二者的區別是:swift

  • @State: 主要用於修飾值類型那種簡單屬性。
  • @StateObject: 和 @ObservedObject 同樣,主要用於引用類型那種複雜屬性。

舉例說明。在一個 SwiftUI View 中聲明屬性:app

@State var name: String

那麼,每次 name 發生變化時,View 都會從新渲染。框架

但假若有一個類:ui

class Student {
    var name: String = ""
}

當它的實例被用 @State 修飾時:3d

@State var student: Student = Student()

則 View 不會隨着 student.name 的變化而變化,由於實例 student 自己並無發生變化。爲了讓 View 隨 student 的屬性變化,就要用到 @StateObject 來包裝:code

@StateObject var student: Student = Student()

同時還要給 Student 的屬性添加 @Published 包裝:視頻

class Student: ObservableObject {
    @Published var name: String = Student()
}

如此,每次 student.name 發生變化時,View 就會隨之從新渲染了。htm

@Binding 和 @ObjectBinding

有時候咱們在子 View 中須要用到父 View 的屬性,而且不單單單方面的顯示,還要有雙向的影響,即子 View 對屬性的更改,能反應到父 View 上。

在如下狀況下:

// Parent View
struct ForumView: View {
    @State var username: String = ""
    
    var body: some View {
        Text(username)
        InputView(name: username)
    }
}

// Child View
struct InputView: View {
    @State var name: String = ""
    
    var body: some View {
        Text(name)
        Button(action: {
            name = "Tom"
        }) {
            Text("Change Name")
        }
    }
}

// Preview: ForumView(username: "Jack")

父 View 雖然能把本身屬性的值傳給子 View,可是子 View 在改變其屬性值時,僅可以改變它自身,而不能影響到父 View。若要影響到父 View,就須要用到 @Binding 了:

// Parent View
struct ForumView: View {
    @State var username: String = ""
    
    var body: some View {
        Text(username)
        InputView(name: $username)
    }
}

// Child View
struct InputView: View {
    @Binding var name: String
    
    var body: some View {
        Text(name)
        Button(action: {
            name = "Tom"
        }) {
            Text("Change Name")
        }
    }
}

// Preview: ForumView(username: "Jack")

此時,你經過子 View 改變的值 (name),就同時也能改變到父 View 的屬性 (username) 了。

簡而言之,@Binding 就是對其餘屬性的一種引用式的綁定。注意用法:它在聲明時,不須要賦初始值,在用到時,要加前綴 $

@Binding 對應 @State,則 @ObjectBinding 便對應 @ObservedObject 和 @StateObject 了,毋庸贅言。

@ObservedObject 和 @StateObject

用 @ObservedObject 和 @StateObject 包裝的屬性都須要其對象類實現 ObservableObject 協議。本質上,他們都是用來讓對象狀態化的包裝器。但在使用時,有必定區別。

簡單地說,@ObservedObject 會在 View 每次被從新渲染時從新構造,它包裝的 Model 是跟着 View 走的,而 @StateObject 則不會,它一旦被建立,就由 SwiftUI 接管,不會隨着 View 的刷新渲染而重建。

爲何會這樣,由於 View 做爲 struct 是一個值類型的對象,他被銷燬時,它內部的對象也會被銷燬,而 @StateObject 等因而給 View 內部的對象加了一層保護,使其不受 View 生命週期的影響。

有時咱們經過 NavigationView 來回切換頁面,會發現 @StageObject 對象也被重置了,像是隨着 View 刷新而重建同樣,其實那是 SwiftUI 的行爲。

比較而言,我以爲 @StateObject 更好,由於它和 View 解耦了,更方便控制。

@EnvironmentObject

@EnvironmentObject 有 @StateObject 那種脫離 View 生命週期的特性,但在使用上更爲靈活。舉例來講:

  • View A: 建立了 @StateObject var thing: Thing,包含 View B
  • View B: 包含 View C
  • View C: 須要用到 View A 的 thing 對象。

通常來講,爲了讓 View C 用到 View A 的 thing,就須要從 View A 開始傳遞 thing 給 View B, 再由 View B 傳給 View C 使用。這是否是太麻煩了,View B 憑空多了一個它用不到但卻能訪問的對象 thing。@EnvironmentObject 的存在就是爲了解決這個問題。

在 View A 中:

var thing: Thing = Thing(tag: "e")
var body: some View {
    NavigationView {
        ViewB()
    }.environmentObject(thing)
}

經過 .environmentObject(), thing 變成了環境對象。接下來咱們在 View C 中就能夠直接使用了:

@EnvironmentObject var thing: Thing
var body: some View {
    // thing.tag: "e"
    Text(thing.tag)
}

能夠看到 View C 中 @EnvironmentObject var thing: Thing 不用初始化 thing,由於這個 thing 就是 ViewA 中的 thing。EnvironmentObject 就像把一個對象全局化了同樣。

參考

本文主要參考瞭如下文章和視頻:

相關文章
相關標籤/搜索