SwiftUI 中常见的属性包装器(Property Wrapper)概览

Demo

@State

由 SwiftUI 管理的可读写的属性包装器,当修饰的属性值改变的时候,界面会随之更新。由 @State 包装的属性通常用 private 修饰,在 body 内使用。

下面的实例是一个可以切换天气的界面,并且可以控制天气是否可变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
enum Weather: String, CaseIterable {
case sun = "Sun"
case cloud = "Cloud"
case rain = "Rain"
case snow = "Snow"

var imageName: String {
switch self {
case .sun:
return "sun.max"
case .cloud:
return "cloud"
case .rain:
return "cloud.rain"
case .snow:
return "snow"
}
}
}

struct StateView: View {
@State private var weather: Weather = .sun
@State private var mutableWeather = false

var body: some View {
VStack {
Toggle(isOn: $mutableWeather) {
Text(mutableWeather ? "Mutable" : "Immutable")
}
.padding()
// add weather view
Spacer()
}
.navigationBarTitle("Weather", displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
if self.mutableWeather {
let random = Int.random(in: 0..<Weather.allCases.count)
self.weather = Weather.allCases[random]
}
}) {
Text(weather.rawValue)
})
}
}

@Binding

接下来,我们用自定义的 WeatherView 去展示天气图片,图片会跟随父视图天气变化做相应改变,并且在 WeatherView 中可以通过点击改变天气。这个时候,我们就需要用到 @binding 来做数据的双向绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct WeatherView: View {
@Binding var weather: Weather
@Binding var mutableWeather: Bool

var body: some View {
Image(systemName: weather.imageName)
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.onTapGesture {
self.mutableWeather = true
let random = Int.random(in: 0..<Weather.allCases.count)
self.weather = Weather.allCases[random]
}
}
}

接下来我们在 // add weather view下面添加如下代码:

1
WeatherView(weather: $weather, mutableWeather: $mutableWeather)

@ObservedObject、@Published、ObservableObject

遵循 ObservableObject 协议的类的属性可以用 @Published 包装,在多个界面之间同步数据,只需要将需要监听的实例对象用 @ObservedObject 包装即可。注意:这里适用的对象类型是 class,因为 class 是在内存中共享数据的。

下面是一个可以编辑一个人姓名和年龄的实例,在 EditView 所做的更改,会同步至 Person 界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Person: ObservableObject {
@Published var name: String
@Published var age: Int

init(name: String, age: Int) {
self.name = name
self.age = age
}
}

struct EditView: View {
@ObservedObject var person: Person

var body: some View {
// TextField 只能绑定 String,需要自定义 Binding
let bindingAge = Binding<String>(get: {
"\(self.person.age)" == "0" ? "" : "\(self.person.age)"
}) { value in
self.person.age = Int(value) ?? 0
}
return Form {
TextField("Input name", text: $person.name)
TextField("Input age", text: bindingAge)
}
}
}

struct ObservedObjectView: View {
@ObservedObject private var person = Person(name: "Bruce", age: 30)

var body: some View {
List {
Text(person.name)
Text("\(person.age)")
}
.navigationBarTitle("Person", displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: EditView(person: self.person)) {
Text("Edit")
}
)
}
}

@Environment、EnvironmentValues

@Environment 可以让我们在 View 中直接访问预设的环境变量,比如系统是否暗黑模式、系统日历、时区等。

下面是一个可以返回上一个页面的实例,@Environment.presentationMode绑定在当前的 View,我们可以直接调用presentationMode来获取它的值:

1
2
3
4
5
6
7
8
9
struct EnvironmentView: View {
@Environment(\.presentationMode) private var presentationMode

var body: some View {
Button("Dismiss") {
self.presentationMode.wrappedValue.dismiss()
}
}
}

系统为我们提供了许多有用的预设变量,详见 https://developer.apple.com/documentation/swiftui/environmentvalues

我们也可以为预设值注入新值,比如我们将返回按钮标题改为 “Dismiss\nDismiss”,这时可以看见一个换行的按钮,而当我们给.lineLimit注入新值得时候,按钮标题就只有一行了:

1
2
3
4
Button("Dismiss\nDismiss") {
self.presentationMode.wrappedValue.dismiss()
}
.environment(\.lineLimit, 1)

这里只是举个例子,我们使用.lineLimit(1)也能达到同样的效果。

我们也可以自定义 EnvironmentValues:

1
2
3
4
5
6
7
8
9
10
struct DismissColorKey: EnvironmentKey {
public static let defaultValue = Color.red
}

extension EnvironmentValues {
var dismissColor: Color {
set { self[DismissColorKey.self] = newValue }
get { self[DismissColorKey.self] }
}
}

然后我们添加一个新的属性:

1
@Environment(\.dismissColor) private var dismissColor

再使用自定义的预设值添加一个红色的返回按钮:

1
2
3
4
5
6
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Red Dismiss")
.foregroundColor(dismissColor)
}

@EnvironmentObject

@EnvironmentObject 和 @ObservedObject 很像,都需要遵循 ObservableObject 协议,都可以同步数据状态,但是它具备更强大的功能,那就是子视图可以自动获取父视图注入的环境变量。

比如我们有如下视图层级:A -> B -> C -> D -> E,后一个是前一个视图的子视图。如果我们使用 @ObservedObject 在 A 视图包装一个变量,我们需要在每个视图包装一个变量,将变量一层层传递到 E 视图。而使用 @EnvironmentObject 我们不需要这么复杂,我们在 A 视图声明一个变量后,在 E 视图用 @EnvironmentObject 包装一个变量后,就可以获取到 A 视图注入的环境变量了,而且可以同步数据的修改,这简直是太方便了。要注意的是,如果 E 视图找不到这个环境变量,程序会崩溃,所以要确保 E 视图能获取到注入的环境变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class User: ObservableObject {
@Published var name = "Bruce"
}

struct ViewA: View {
var body: some View {
ViewB()
.frame(width: 300, height: 300)
.background(Color.red)
}
}

struct ViewB: View {
var body: some View {
ViewC()
.frame(width: 200, height: 200)
.background(Color.black)
}
}

struct ViewC: View {
@EnvironmentObject var user: User

var body: some View {
Text(user.name)
.frame(width: 100, height: 100)
.background(Color.white)
}
}

struct EnvironmentObjectView: View {
private let user = User()

var body: some View {
ViewA().environmentObject(user)
}
}

Demo

您的支持将鼓励我继续创作!
0%