SwiftUI:观察@Environment属性更改

时间:2020-01-16 15:48:02

标签: ios swift swiftui

我试图使用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。我确定我丢失了一些内容在这里,这是怎么回事?谢谢。

2 个答案:

答案 0 :(得分:10)

Environment使您可以访问存储在EnvironmentKey下的内容,但不会为其内部生成观察者(即,如果EnvironmentKey的值本身发生更改,系统会通知您,但在您的情况下,它是实例并且其在key下存储的引用不变)。因此,它需要手动进行观察,因为您是否有发布者在那儿,如下所示?

@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测试

答案 1 :(得分:1)

对于 Apple 如何动态地向其标准 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 键/值。这是现有代码之间的第一个区别。 步骤 1 中的每个单独的 IntGenerator 属性都将有一个 EnvironmentKey,而不是使用 @Published

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 应用)。这是我们如何在 Views 属性更改时动态更新 @Published 的秘诀。这个简单的包装器 View 将保留 IntGenerator 的实例并在我们的 EnvironmentValues.int 属性值更改时更新我们的 @Published

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")
    }
}

这是我们如何使用 new 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)
        }
    }
}

第四步) 最后是我们如何在需要访问 EnvironmentKey 的任何 View 中实际使用新的 int。每当我们的 View 类上的 int 值更新时,此 IntGenerator 都会自动重建!

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

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