主页 理解 SwiftUI 中的属性包装器
Post
Cancel

理解 SwiftUI 中的属性包装器

SwiftUI 为我们提供了 ` @State@Binding@ObservedObject@EnvironmentObject@Environment` 属性包装器。因此,让我们尝试了解它们之间的区别以及我们必须使用哪个以及何时以及为什么。

属性包装器

SE-0258提案中描述的属性包装器功能。这里的主要目标是用逻辑包装属性,这些逻辑可以提取到分离的结构中,以便在代码库中重用它。

@State

@State是一个Property Wrapper,我们可以用它来描述View的状态。SwiftUI 会将其存储在View结构之外的特殊内部存储器中。只有相关的View才能访问它。只要@State 属性的值发生变化,SwiftUI 就会重建 View以尊重状态变化。这是一个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ProductsView: View {
    let products: [Product]

    @State private var showFavorited: Bool = false

    var body: some View {
        List {
            Button(
                action: { self.showFavorited.toggle() },
                label: { Text("Change filter") }
            )

            ForEach(products) { product in
                if !self.showFavorited || product.isFavorited {
                    Text(product.title)
                }
            }
        }
    }
}

在上面的示例中,我们有一个带有 ButtonList 的简单屏幕。只要我们按下按钮,它就会更改 state 属性的值,并且 SwiftUI 会重新创建View

@Binding

@Binding为值类型提供类似访问的引用。有时我们需要让View的状态可供其子项访问。但是我们不能简单地传递那个值,因为它是一个值类型,Swift 会传递那个值的副本。这就是我们可以使用@Binding Property Wrapper的地方。

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
struct FilterView: View {
    @Binding var showFavorited: Bool

    var body: some View {
        Toggle(isOn: $showFavorited) {
            Text("Change filter")
        }
    }
}

struct ProductsView: View {
    let products: [Product]

    @State private var showFavorited: Bool = false

    var body: some View {
        List {
            FilterView(showFavorited: $showFavorited)

            ForEach(products) { product in
                if !self.showFavorited || product.isFavorited {
                    Text(product.title)
                }
            }
        }
    }
}

我们使用 @Binding 来标记 FilterView 内的showFavorited属性。我们还使用$来传递绑定引用,因为没有$ Swift 将传递值的副本而不是传递可绑定引用。FilterView 可以读取和写入ProductsView的showFavorited属性的值。一旦FilterView改变了showFavorited属性的值,SwiftUI 就会重新创建ProductsViewFilterView作为它的孩子。

@Binding为类型提供了类似访问的引用value。这就是为什么它应该只与值类型一起使用。如果ValueBinding 不是值语义,Binding则未指定使用结果的任何视图的更新行为。

@ObservedObject

我们应该使用@ObservedObject 来处理SwiftUI之外的数据,比如你的业务逻辑。我们可以在多个独立的Views之间共享它,这些 Views可以订阅和观察该对象的更改,并且一旦更改出现,SwiftUI 就会重建绑定到该对象的所有Views 。让我们看一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
import Combine

final class PodcastPlayer: ObservableObject {
    @Published private(set) var isPlaying: Bool = false

    func play() {
        isPlaying = true
    }

    func pause() {
        isPlaying = false
    }
}

在这里,我们有PodcastPlayer类,我们在应用程序的屏幕之间共享它。当应用程序正在播放播客剧集时,每个屏幕都必须显示浮动暂停按钮。SwiftUI 在@Published属性包装器的帮助下跟踪ObservableObject上的更改,一旦标记为@Published的属性发生更改,SwiftUI 就会重建绑定到该PodcastPlayer对象的所有视图。这里我们使用@ObservedObject Property Wrapper将我们的EpisodesView绑定到PodcastPlayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct EpisodesView: View {
    @ObservedObject var player: PodcastPlayer
    let episodes: [Episode]

    var body: some View {
        List {
            Button(
                action: {
                    if self.player.isPlaying {
                        self.player.pause()
                    } else {
                        self.player.play()
                    }
            }, label: {
                    Text(player.isPlaying ? "Pause": "Play")
                }
            )
            ForEach(episodes) { episode in
                Text(episode.title)
            }
        }
    }
}

请记住,我们可以ObservableObject在多个视图之间共享,这就是为什么它必须是reference type/class.

@EnvironmentObject (环境对象)

我们可以将 ObservableObject 隐式注入到View层次结构的Environment中,而不是通过 View 的 init 方法传递ObservableObject 。通过这样做,我们为当前环境的所有子视图访问此ObservableObject创造了机会。

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
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let window = UIWindow(frame: UIScreen.main.bounds)
        let episodes = [
            Episode(id: 1, title: "First episode"),
            Episode(id: 2, title: "Second episode")
        ]

        let player = PodcastPlayer()
        window.rootViewController = UIHostingController(
            rootView: EpisodesView(episodes: episodes)
                .environmentObject(player)
        )
        self.window = window
        window.makeKeyAndVisible()
    }
}

struct EpisodesView: View {
    @EnvironmentObject var player: PodcastPlayer
    let episodes: [Episode]

    var body: some View {
        List {
            Button(
                action: {
                    if self.player.isPlaying {
                        self.player.pause()
                    } else {
                        self.player.play()
                    }
            }, label: {
                    Text(player.isPlaying ? "Pause": "Play")
                }
            )
            ForEach(episodes) { episode in
                Text(episode.title)
            }
        }
    }
}

如您所见,我们可以通过View的environmentObject修饰符传递PodcastPlayer对象。通过这样做,我们可以通过使用@EnvironmentObject Property Wrapper定义PodcastPlayer轻松访问它。@EnvironmentObject使用动态成员查找功能在 Environment 中查找PodcastPlayer类实例,这就是为什么您不需要通过 EpisodesView 的 init 方法传递它的原因。它像魔术一样工作。

@Environment (环境)

正如我们在上一章中所讨论的,我们可以将自定义对象传递到SwiftUI中View层次结构的Environment中。但是 SwiftUI 已经有一个填充了系统范围设置的环境。我们可以使用@Environment Property Wrapper轻松访问它们。

1
2
3
4
5
6
7
8
9
struct CalendarView: View {
    @Environment(\.calendar) var calendar: Calendar
    @Environment(\.locale) var locale: Locale
    @Environment(\.colorScheme) var colorScheme: ColorScheme

    var body: some View {
        return Text(locale.identifier)
    }
}

通过使用@Environment Property Wrapper标记我们的属性,我们可以访问和订阅系统范围设置的更改。一旦系统的LocaleCalendarColorScheme发生变化,SwiftUI 就会重新创建我们的CalendarView

结论

今天我们讲了 SwiftUI 提供的Property Wrappers@State@Binding@EnvironmentObject@Environment@ObservedObject在 SwiftUI 开发中发挥着巨大的作用。

该博客文章由作者通过 CC BY 4.0 进行授权。

文章目录

SwiftUI如何使用 ColorScheme 为浅暗模式配置图像

SwiftUI 中@StateObject、@EnvironmentObject 和@ObservedObject 的区别