SwiftUI 可选环境对象

24

我这样使用 @EnvironmentObject

struct MyView: View {
  @EnvironmentObject var object: MyObject

  ...
}

但我的代码不需要object有一个值。

仅仅将其设为可选是行不通的(甚至无法编译 - 属性类型 'MyObject?' 与其包装类型 'EnvironmentObject' 的 'wrappedValue' 属性不匹配)。

你也不能传入一个默认对象(这也可以解决我的问题) - 无论是作为属性的初始值,还是作为@EnvironmentObject的参数。例如这些都不行:

@EnvironmentObject var object: MyObject = MyObject()

@EnvironmentObject(MyObject()) var object: MyObject

我试图在我的自定义属性包装器中包装@EnvironmentObject,但根本不起作用。

我还尝试包装对该对象属性的访问,但它不会抛出可以捕获的异常,而是抛出fatalError

我是否遗漏了什么,或者我只是在尝试不可能的事情?


1
我有一个反问... 如果不需要的话,为什么要使用@EnvironmentObject呢?为什么不直接使用@ObservedObject,它可以有默认实例并且是可选的呢? - Asperi
3
因为接下来我需要传递一个东西 - 理想情况下,如果环境中存在额外的功能,我希望它具有更多功能,但是我不想在各个地方都传递这个东西(我的用例需要在很多地方传递) - deanWombourne
5个回答

11

通过符合 EnvironmentKey,您可以提供默认值,在缺少值的情况下 SwiftUI 可以安全地回退到该默认值。此外,您还可以利用EnvironmentValues 通过 key path API 访问对象。

您可以像下面这样将它们组合起来使用:

public struct ObjectEnvironmentKey: EnvironmentKey {
    // this is the default value that SwiftUI will fallback to if you don't pass the object
    public static var defaultValue: Object = .init()
}

public extension EnvironmentValues {
    // the new key path to access your object (\.object)
    var object: Object {
        get { self[ObjectEnvironmentKey.self] }
        set { self[ObjectEnvironmentKey.self] = newValue }
    }
}

public extension View {
    // this is just an elegant wrapper to set your object into the environment
    func object(_ value: Object) -> some View {
        environment(\.object, value)
    }
}

现在要从视图中访问您的新对象:

struct MyView: View {
    @Environment(\.object) var object
}

祝您愉快!


2
这是响应式的吗? - Nico Dioso
2
这不是响应式的。@NicoDioso - Connor

11

这并不是非常优雅的写法,如果EnvironmentObject中的任何内容发生变化(以及其他注意事项),它很容易出现错误。但是,在SwiftUI 1 / Xcode 11.3.1中打印EnvironmentObject,您会得到:

EnvironmentObject<X>(_store: nil, _seed: 1)

那么,你觉得这样怎么样:

extension EnvironmentObject {
    var hasValue: Bool {
        !String(describing: self).contains("_store: nil")
    }
}

13
哈哈哈,这太糟糕了,但又是最好的方式! - deanWombourne

6

我知道你说过无法将对象放入包装器中,但是我认为这种解决方案是实现你想要的内容的好方法。

你需要做的唯一事情是创建一个非可选的包装器,但它将包含你的可选对象:

class MyObjectWrapper: ObservableObject {

  @Published var object: MyObject?

}

然后,您需要创建视图并将包装器分配给环境:
let wrapper = MyObjectWrapper()
// You can try to load your object here, and set it to nil if needed.
let view = MyView().environmentObject(wrapper)

在您的视角中,您现在可以检查您的对象是否存在:
struct MyView: View {
  
  @EnvironmentObject var objectWrapper: MyObjectWrapper
  
  var body: some View {
    if objectWrapper.object != nil {
      Text("Not nil")
    } else {
      Text("Nil")
    }
  }
  
}

如果任何视图更改objectWrapper.object,则视图将重新加载。
您可以轻松模拟视图,甚至在几秒钟后触发更改以检查过渡:
struct MyView_Previews: PreviewProvider {

  static var previews: some View {
    let objectWrapper = MyObjectWrapper()
    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
      objectWrapper.object = MyObject()
    }
    return MyView().environmentObject(objectWrapper)
  }

}

2

我基于StateObject和通过@autoscaping闭包懒初始化默认值的方法,创建了一个包装器。

@EnvironmentModel var object = Object() //default value

此外,如果您通过环境传递对象,则无需将其存储在某个位置。
yourView.environmentModel(Object())

代码

@propertyWrapper
public struct EnvironmentModel<Model: ObservableObject>: DynamicProperty {

    @StateObject private var object = Object()
    @Environment(\.environmentModel) private var environment
    private let defaultValue: () -> Model
    private let id: AnyHashable

    public var wrappedValue: Model {
        createModel()
        return object.model
    }

    public var projectedValue: Binding<Model> {
        createModel()
        return $object.model
    }

    public init(wrappedValue: @escaping @autoclosure () -> Model) {
        defaultValue = wrappedValue
        id = String(reflecting: Model.self)
    }

    public init<ID: Hashable>(wrappedValue: @escaping @autoclosure () -> Model, _ id: ID) {
        defaultValue = wrappedValue
        self.id = id
    }

    @inline(__always) private func createModel() {
        guard object.model == nil else { return }
        object.model = (environment[id] as? () -> Model)?() ?? defaultValue()
    }

    private final class Object: ObservableObject {
        var model: Model! {
            didSet {
                model.objectWillChange.subscribe(objectWillChange).store(in: &bag)
            }
        }
        var bag: Set<AnyCancellable> = []
        let objectWillChange = PassthroughSubject<Model.ObjectWillChangePublisher.Output, Model.ObjectWillChangePublisher.Failure>()
    
        init() {}
    }
}

extension View {
    public func environmentModel<M: ObservableObject>(_ model: @escaping @autoclosure () -> M) -> some View {
        modifier(EnvironmentModelModifier(model: model, key: String(reflecting: M.self)))
    }

    public func environmentModel<M: ObservableObject, ID: Hashable>(id: ID, _ model: @escaping @autoclosure () -> M) -> some View {
        modifier(EnvironmentModelModifier(model: model, key: id))
    }
}

private struct EnvironmentModelModifier<Model>: ViewModifier {
    @State private var object = Object()
    private let create: () -> Model
    let key: AnyHashable

    var model: Model {
        createModel()
        return object.model
    }

    init(model: @escaping () -> Model, key: AnyHashable) {
        create = model
        self.key = key
    }

    @inline(__always) private func createModel() {
        guard object.model == nil else { return }
        object.model = create()
    }

    func body(content: Content) -> some View {
        let value: () -> Model = { self.model }
        return content.environment(\.environmentModel[key], value)
    }

    private final class Object {
        var model: Model!
    
        init() {}
    }
}

private enum EnvironmentModelKey: EnvironmentKey {
    static var defaultValue: [AnyHashable: Any] { [:] }
}

extension EnvironmentValues {
    fileprivate var environmentModel: [AnyHashable: Any] {
        get { self[EnvironmentModelKey.self]  }
        set { self[EnvironmentModelKey.self] = newValue }
    }
}

0
这个简短的属性包装器将跟踪保存在Environment中的ObservableObject的更改。它使用DynamicProperty.update()来开始跟踪通过Environment传入的对象。
原始问题是关于一个空对象的。如果需要,您可以将var object: Object更改为可选项。
@propertyWrapper
public struct EnvironmentModel<Object: ObservableObject>: DynamicProperty {
    
    @Environment var object: Object
    @StateObject private var observer = Observer()
    
    public init(_ keyPath: KeyPath<EnvironmentValues, Object>) {
        self._object = Environment(keyPath)
    }
    
    public var wrappedValue: Object {
        self.object
    }
    
    public func update() {
        self.observer.cancellable = self.object.objectWillChange.sink { [weak observer] _ in
            observer?.objectWillChange.send()
        }
    }
    
    private class Observer: ObservableObject {
        var cancellable: AnyCancellable?
    }
    
}

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