欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

理解SwiftUI的Property Wrapper

程序员文章站 2024-03-24 12:46:10
...

原文: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 框架