【问题标题】:SwiftUI: observe @Environment property changesSwiftUI:观察@Environment 属性变化
【发布时间】: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,否则视图不会更新。谢谢你的提示

标签: ios swift swiftui


【解决方案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 如何通过colorScheme、horizontalSizeClass 等活动实现这一目标?
【解决方案2】:

对于 Apple 如何准确地动态发送更新到其标准 Environment 键(colorScheme、horizontalSizeClass 等),我没有明确的答案,但我确实有一个解决方案,我怀疑 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 中工作/测试

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-07-29
    • 2011-05-27
    • 1970-01-01
    • 2020-03-21
    • 1970-01-01
    • 2018-04-10
    • 2013-10-24
    • 2017-09-07
    相关资源
    最近更新 更多