SwiftUI 中一些和響應式狀態有關的屬性包裝器的用途
阿新 • • 發佈:2021-01-21
SwiftUI 借鑑了 React 等 UI 框架的概念,通過 state 的變化,對 View 進行響應式的渲染。主要通過 @State, @StateObject, @ObservedObject 和 @EnvironmentObject 等屬性包裝器 (property wrapper) 將屬性包裝成狀態來實現。
## @State 和 @StateObject
@State 和 @StateObject 是比較常用的屬性包裝器。
兩者的區別是:
- @State: 主要用於修飾值型別那種簡單屬性。
- @StateObject: 和 @ObservedObject 一樣,主要用於引用型別那種複雜屬性。
舉例說明。在一個 SwiftUI View 中宣告屬性:
```swift
@State var name: String
```
那麼,每次 name 發生變化時,View 都會重新渲染。
但假如有一個類:
```swift
class Student {
var name: String = ""
}
```
當它的例項被用 @State 修飾時:
```swift
@State var student: Student = Student()
```
則 View 不會隨著 `student.name` 的變化而變化,因為例項 student 本身並沒有發生變化。為了讓 View 隨 student 的屬性變化,就要用到 @StateObject 來包裝:
```swift
@StateObject var student: Student = Student()
```
同時還要給 Student 的屬性新增 @Published 包裝:
```swift
class Student: ObservableObject {
@Published var name: String = Student()
}
```
如此,每次 `student.name` 發生變化時,View 就會隨之重新渲染了。
## @Binding 和 @ObjectBinding
有時候我們在子 View 中需要用到父 View 的屬性,並且不僅僅單方面的顯示,還要有雙向的影響,即子 View 對屬性的更改,能反應到父 View 上。
在以下情況下:
```swift
// 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 了:
```swift
// 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 中:
```swift
var thing: Thing = Thing(tag: "e")
var body: some View {
NavigationView {
ViewB()
}.environmentObject(thing)
}
```
通過 `.environmentObject()`, thing 變成了環境物件。接下來我們在 View C 中就可以直接使用了:
```swift
@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 就像把一個物件全域性化了一樣。
## 參考
本文主要參考了以下文章和視訊:
- [@StateObject 和 @ObservedObject 的區別和使用](https://onevcat.com/2020/06/stateobject/)
- [SwiftUI: @State vs @StateObject vs @ObservedObject vs @EnvironmentObject](https://levelup.gitconnected.com/state-vs-stateobject-vs-observedobject-vs-environmentobject-in-swiftui-81e2913d63f9)
- [When Should I Use @State, @Binding, @ObservedObject, @EnvironmentObject, or @Environment?](https://jaredsinclair.com/2020/05/07/swiftui-cheat-sheet.html)
- [Sharing data across tabs using @EnvironmentObject – Hot Prospects SwiftUI Tutorial 10/16](https://www.youtube.com/watch?v=1Qcek