我试图使用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。我确定我丢失了一些内容在这里,这是怎么回事?谢谢。
答案 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
键(colorScheme
、horizontalSizeClass
等)发送更新,我没有明确的答案,但我确实有一个解决方案,并且我怀疑苹果在幕后做了类似的事情。
第一步)为您的值创建一个 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 中工作/测试