SwiftUI:观察@Environment 属性变化

Posted

技术标签:

【中文标题】SwiftUI:观察@Environment 属性变化【英文标题】:SwiftUI: observe @Environment property changes 【发布时间】:2020-01-16 15:48:02 【问题描述】:

我试图使用 SwiftUI @Environment 属性包装器,但我无法让它按预期工作。请帮助我理解我做错了什么。

例如,我有一个每秒产生一次整数的对象:

class IntGenerator: ObservableObject 
    @Published var newValue = 0 
        didSet 
            print(newValue)
        
    

    private var toCanc: AnyCancellable?

    init() 
        toCanc = Timer.TimerPublisher(interval: 1, runLoop: .main, mode: .default)
            .autoconnect()
            .map  _ in Int.random(in: 0..<1000) 
            .assign(to: \.newValue, on: self)
    

这个对象按预期工作,因为我可以看到控制台日志上生成的所有整数。现在,假设我们希望这个对象是一个环境对象,可以从整个应用程序和任何人访问。让我们创建相关的环境密钥:

struct IntGeneratorKey: EnvironmentKey 
    static let defaultValue = IntGenerator()


extension EnvironmentValues 
    var intGenerator: IntGenerator 
        get 
            return self[IntGeneratorKey.self]
        
        set 
            self[IntGeneratorKey.self] = newValue
        
    

现在我可以像这样访问这个对象(例如从视图中):

struct TestView: View 
    @Environment(\.intGenerator) var intGenerator: IntGenerator

    var body: some View 
        Text("\(intGenerator.newValue)")
    

不幸的是,尽管newValue@Published 属性,但我没有收到有关该属性的任何更新,并且Text 始终显示为0。我确定我在这里遗漏了一些东西,这是怎么回事?谢谢。

【问题讨论】:

我很困惑,为什么使用环境与环境对象?您可以使用它和发布商 @ColinWhooten EnvironmentObject 用于在视图层次结构中注入对象,并使这些对象在视图层次结构内的所有视图中可用。 Environment 与视图没有严格的关系。它只是在您的项目中创建一个全局依赖项。当您创建 Environment 依赖项时,您可以从应用程序中的所有实体(不仅仅是视图)访问该依赖项。所以,我想知道我是否可以在Environment 对象中观察Published 属性。 我遇到了同样的问题,但我使用的是EnvironmentObject,里面有ObservedObject。除非我将 ObservedObject 更改为 Published,否则视图不会更新。谢谢你的提示 【参考方案1】:

Environment 让您可以访问存储在EnvironmentKey 下的内容,但不会为其内部生成观察者(即,如果 EnvironmentKey 的值自身发生变化,您会收到通知,但在您的情况下,它是实例并且其引用已存储下键不变)。所以它需要手动观察,你那里有发布者吗,如下所示

@Environment(\.intGenerator) var intGenerator: IntGenerator

@State private var value = 0
var body: some View 
    Text("\(value)")
        .onReceive(intGenerator.$newValue)  self.value = $0 

一切正常...使用 Xcode 11.2 / ios 13.2 测试

【讨论】:

这太棒了!你是怎么想到的? Apple 如何通过colorSchemehorizontalSizeClass 等活动实现这一目标?【参考方案2】:

对于 Apple 如何准确地动态发送更新到其标准 Environment 键(colorSchemehorizontalSizeClass 等),我没有明确的答案,但我确实有一个解决方案,我怀疑 Apple 在背后做了类似的事情场景。

第一步)为您的值创建一个带有@Published 属性的ObservableObject

class IntGenerator: ObservableObject 
    
    @Published var int = 0
    private var cancellables = Set<AnyCancellable>()
    
    init() 
        Timer.TimerPublisher(interval: 1, runLoop: .main, mode: .default)
            .autoconnect()
            .map  _ in Int.random(in: 0..<1000) 
            .assign(to: \.int, on: self)
            .store(in: &cancellables)
    
    

第二步)为您的媒体资源创建一个自定义的Environment 键/值。这是现有代码之间的第一个区别。 步骤 1 中的每个 @Published 属性将有一个 EnvironmentKey,而不是使用 IntGenerator

struct IntKey: EnvironmentKey 
    static let defaultValue = 0


extension EnvironmentValues 
    var int: Int 
        get 
            return self[IntKey.self]
        
        set 
            self[IntKey.self] = newValue
        
    

第三步 - UIHostingController 方法) 如果您使用 App Delegate 作为您的生命周期(也就是带有 Swift UI 功能的 UIKit 应用)。这是当我们的@Published 属性发生变化时我们如何能够动态更新我们的Views 的秘诀。这个简单的包装器View 将保留IntGenerator 的一个实例,并在我们的@Published 属性值更改时更新我们的EnvironmentValues.int

struct DynamicEnvironmentView<T: View>: View 
    
    private let content: T
    @ObservedObject var intGenerator = IntGenerator()
    
    public init(content: T) 
        self.content = content
    
    
    public var body: some View 
        content
            .environment(\.int, intGenerator.int)
    

让我们通过创建自定义UIHostingController 并利用我们的DynamicEnvironmentView 轻松地将其应用于整个功能的视图层次结构。该子类自动将您的内容包装在 DynamicEnvironmentView 中。

final class DynamicEnvironmentHostingController<T: View>: UIHostingController<DynamicEnvironmentView<T>> 
    
    public required init(rootView: T) 
        super.init(rootView: DynamicEnvironmentView(content: rootView))
    
    
    @objc public required dynamic init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

这是我们如何使用新的DynamicHostingController

let contentView = ContentView()
window.rootViewController = DynamicEnvironmentHostingController(rootView: contentView)

第三步 - 纯 Swift UI 应用方法) 如果您使用的是纯 Swift UI 应用。在此示例中,我们的 App 保留了对 IntGenerator 的引用,但您可以在此处尝试不同的架构。

@main
struct MyApp: App 
    
    @ObservedObject var intGenerator = IntGenerator()
    
    var body: some Scene 
        WindowGroup 
            ContentView()
                .environment(\.int, intGenerator.int)
        
    

第四步) 最后是我们如何在需要访问int 的任何View 中实际使用新的EnvironmentKey。只要 IntGenerator 类的 int 值更新,这个View 就会自动重建!

struct ContentView: View 
    
    @Environment(\.int) var int
    
    var body: some View 
        Text("My Int Value: \(int)")
    

在 Xcode 12.2 上的 iOS 14 中工作/测试

【讨论】:

以上是关于SwiftUI:观察@Environment 属性变化的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI:属性装饰器的理解@State,@Binding,@ObservedObject,@Published,@Environment,@EnvironmentObject

观察自定义视图 SwiftUI 中的属性变化

调用 @Environment 时收到错误“类 'Environment' 不能用作属性”

SwiftUI:@Environment 未在视图层次结构中接收提供的值

过滤后的 SwiftUI CoreData 列表中的 Sum 属性

如何在 SwiftUI 中使用可观察对象并与 NavigationLink 绑定?