理解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,本文有关问题请推我。感谢阅读,下周再见。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值