SwiftUI: 观察 @Environment 属性的变化

20

我试图使用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。我确定我在这里漏了什么,发生了什么?谢谢。


我有点困惑,为什么要使用environment而不是environmentobject?你可以使用它和一个publisher - Colin Whooten
2
@ColinWhooten EnvironmentObject 是用于在视图层次结构中注入对象,并使这些对象在视图层次结构内的所有视图中可用。Environment 不严格相关于视图。它只是为了在您的项目中创建全局依赖项。当您创建一个 Environment 依赖项时,您可以从应用程序中的所有实体(而不仅仅是视图)访问该依赖项。因此,我想了解是否可以观察 Environment 对象中的 Published 属性。 - superpuccio
我曾经遇到过同样的问题,但是我在使用EnvironmentObject和内部的ObservedObject。视图没有更新,除非我将ObservedObject更改为Published。感谢您的提示。 - ramzesenok
2个回答

37

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上进行了测试


这太棒了!你是怎么想到的? - Orkhan Alikhanov
1
苹果是如何在其活动中实现 colorSchemehorizontalSizeClass等功能的? - anders
很棒的回答@Asperi,但是你能否解释一下为什么视图不会自动重绘? - Sotiris Kaniras
@anders,因为它们具有值语义,所以视图将检测到值的更改。在上面的示例中,IntGenerator是一个对象的引用,而该引用不会改变 - 只有对象的属性发生更改。这不会自动检测到。 - Martin

3

我没有确切的答案来解释苹果是如何动态发送更新到它的标准Environment键(colorSchemehorizontalSizeClass等),但我有一个解决方案,并且我怀疑苹果在幕后做了类似的事情。

步骤一)创建一个ObservableObject,其中包含用于您的值的@Published属性。

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)键/值对于您的属性。这里是您现有代码与之不同的第一个区别。您将使用每个单独的@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方法(如果您使用应用程序委托作为生命周期(即带有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)
        }
    }
}

第四步) 最后,这里是我们如何在任何需要访问 intView 中使用我们的新 EnvironmentKey。这个 View 将自动在我们的 IntGenerator 类上更新 int 值时重新构建!

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

本代码已在iOS 14和Xcode 12.2上进行过测试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接