理解SwiftUI的Property Wrapper
原文:Understanding Property Wrappers in SwiftUI
12 Jun 2019
上周,我们介绍了一系列关于 SwiftUI 框架的新帖子。今天,我将继续这个话题,介绍 SwiftUI 的属性包装器 Property Wrapper。SwiftUI 提供的属性包装器包括 @State, @Binding, @ObjectBinding, @EnvironmentObject, 和 @Environment 。我们必须了解它们的区别以及何时使用哪一个包装器。
属性包装器
属性包装器的概念首先是从 SE-0258 提议中提出的。主要目的是将一些封装属性的逻辑从不同的结构中抽离出来,并复用到整个代码库中。这个提议苹果并未接受,但在 Xcode beta 的 Swift 5.1 快照中就有它了。
@State
@State 属性包装器可以用于标记视图的状态。SwiftUI 会将它保存到位于视图结构之外的、特殊的框架内部内存区域。只有关联的视图及其子视图能够访问它。当@State 属性值改变,SwiftUI 会重构与之相关的视图。来看个例子。
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)
}
}
}
}
}
在例子中,我们有一个简单的页面,上面有一个按钮和一个产品列表。当我们按下按钮,它会修改 @State 属性,从而导致 SwiftUI 重绘视图。
@Binding
@Binding 属性会返回一个引用,允许我们访问一个值类型。有时候我们会想让视图的状态能够被它的子视图所访问。但我们不能直接将这个值传递过去,因为 Swift对于值类型会传递拷贝。所以我们就可以使用这个 @Binding 属性包装器来实现。
struct FilterView: View {
@Binding var showFavorited: Bool
var body: some View {
Toggle(isOn: $showFavorited) {
return 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 会重建 ProductsView 及其子视图 FilterView。
@ObjectBinding
@ObjectBinding 的机制和 @State 属性包装器类似,只不过我们可以在多个订阅该对象的视图之间共享它,当改变发生时,SwiftUI 会重建所有绑定到这个对象上的视图。我们可以看个例子。
final class PodcastPlayer: BindableObject {
var isPlaying: Bool = false {
didSet {
didChange.send(self)
}
}
func play() {
isPlaying = true
}
func pause() {
isPlaying = false
}
var didChange = PassthroughSubject<PodcastPlayer, Never>()
}
PodcastPlayer 类在整个 app 的所有窗口之间共享。当有播客播放时,每个窗口都需要显示浮动的暂停按钮。通过 didChange 属性,SwiftUI 会监听 BindableObject 上的改变,唯一需要的就是继承 BindableObject 协议。关于 BindableObject,请参考我上一篇帖子。
struct EpisodesView: View {
@ObjectBinding 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)
}
}
}
}
这里使用额了 @ObjectBinding 属性包装器将我们的 EpisodesView 和 PodcastPlayer 类绑定到一起,当当前视图和其它关联到 PodcastPlayer 对象的视图改变了它时,SwiftUI 会重建所有和 PodcastPlayer 绑定的视图。
@EnvironmentObject
除了可以在视图的 init 方法中传入 BindableObject 之外,我们还可以将它隐含地注入到视图树的环境中去。这样,我们就能够允许当前环境中的所有子视图都能够访问到 BindableObject。
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)
}
}
}
}
如你所见,我们必须用视图的 environmentObject 方法将 PodcastPlayer 对象注入进去。这样,就可以通过声明一个 @EnvironmentObject 属性包装器来直接访问 PodcastPlayer。@EnvironmentObject 使用动态成员查找环境中的 PodcastPlayer 类实例。所以你就再也不需要通过 EpicodesView 的 init 方法中来传入它了。在 SwiftUI 中,依赖注入方法就是通过环境。很神奇吧!
@Environment
在上一篇中我们说过,我们可以向 SwiftUI 的视图树中传递自定义对象到环境中。当然,SwiftUI 已经有一个包含了系统设置的 Environment 对象。我们可以通过 @Environment 属性包装器来访问它。
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 属性包装器,就可以访问和订阅系统设置了。比如 当 Locale、日历和色彩方案发生改变时,SwiftUI 会重建我们的 CalendarView。
结论
我们今天讨论了 SwiftUI 的属性包装器。@State、@Binding、@EnvironmentObject 和 @ObjectBinding 在 SwiftUI 开发中扮演了重要角色。请关注我的 Twitter,本文有关问题请推我。感谢阅读,下周再见。
上一篇: Spring IOC
下一篇: FRAME 框架