简体   繁体   English

SwiftUI 可选环境对象

[英]SwiftUI optional environment object

I'm using @EnvironmentObject like this:我像这样使用@EnvironmentObject

struct MyView: View {
  @EnvironmentObject var object: MyObject

  ...
}

but my code doesn't need there to be a value for object .但我的代码不需要object的值。

Just making this optional doesn't work (doesn't even compile - Property type 'MyObject?' does not match that of the 'wrappedValue' property of its wrapper type 'EnvironmentObject' )只是使这个可选不起作用(甚至不编译 - Property type 'MyObject?' does not match that of the 'wrappedValue' property of its wrapper type 'EnvironmentObject'

You also can't pass in a default object (that would solve my problem too) - either as an initial value to the property, or as a parameter to @EnvironmentObject .您也不能传入默认对象(这也可以解决我的问题) - 作为属性的初始值,或作为@EnvironmentObject的参数。 ei these don't work: ei 这些不起作用:

@EnvironmentObject var object: MyObject = MyObject()

@EnvironmentObject(MyObject()) var object: MyObject

I've tried to wrap the @EnvironmentObject in my own property wrapper, but that just doesn't work at all.我试图将@EnvironmentObject包装在我自己的属性包装器中,但这根本不起作用。

I've also tried wrapping accesses to the object property, but it doesn't throw an exception which can be caught, it throws a fatalError .我也试过包装对 object 属性的访问,但它不会抛出一个可以被捕获的异常,它会抛出一个fatalError

Is there anything I'm missing, or am I just trying the impossible?有什么我遗漏的吗,或者我只是在尝试不可能的事情?

It's not a very elegant and could easily break if anything in EnvironmentObject changes (and other caveats), but if you print EnvironmentObject in SwiftUI 1 / Xcode 11.3.1 you get:如果 EnvironmentObject 中的任何内容发生变化(和其他警告),它不是很优雅并且很容易中断,但是如果您在 SwiftUI 1 / Xcode 11.3.1 中打印 EnvironmentObject,您会得到:

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

so how about:那么怎么样:

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

By conforming to EnvironmentKey you basically can provide a default value that SwiftUI can safely fallback to in case of missing.通过符合EnvironmentKey您基本上可以提供一个默认值,SwiftUI 可以在丢失的情况下安全地回退到该默认值。 Additionally, you can also leverage EnvironmentValues to access the object via key path based API.此外,您还可以利用EnvironmentValues通过基于密钥路径的 API 访问对象。

You can combine both with something like this:您可以将两者与以下内容结合使用:

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

Now to access your new object from a view:现在从视图访问您的新对象:

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

Enjoy!享受!

I know you told you was not able to put your object into a wrapper, however I think this solution is a great way to achieve what you want.我知道您告诉过您无法将对象放入包装器中,但是我认为此解决方案是实现您想要的目标的好方法。

The only thing you have to do, is to create a wrapper that will be non optional, but that will contain your optional object:您唯一要做的就是创建一个非可选的包装器,但它将包含您的可选对象:

class MyObjectWrapper: ObservableObject {

  @Published var object: MyObject?

}

Then, you create your view and assign the wrapper to the environment:然后,您创建视图并将包装器分配给环境:

let wrapper = MyObjectWrapper()
// You can try to load your object here, and set it to nil if needed.
let view = MyView().environmentObject(wrapper)

In your view, you can now check the existence of your object:在您看来,您现在可以检查对象是否存在:

struct MyView: View {
  
  @EnvironmentObject var objectWrapper: MyObjectWrapper
  
  var body: some View {
    if objectWrapper.object != nil {
      Text("Not nil")
    } else {
      Text("Nil")
    }
  }
  
}

If any view change objectWrapper.object , the view will be reloaded.如果任何视图更改objectWrapper.object ,视图将被重新加载。

You can mock your view easily, and even trigger a change after a few seconds to check the transition:您可以轻松模拟您的视图,甚至在几秒钟后触发更改以检查转换:

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

}

I made a wrapper based on StateObject and lazy initialization of a default value via @autoscaping closure.我制作了一个基于StateObject的包装器,并通过@autoscaping闭包延迟初始化默认值。

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

Also, if you pass an object through an environment, it does not have to be stored somewhere此外,如果您通过环境传递对象,则不必将其存储在某处

yourView.environmentModel(Object())

Code代码

@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 }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM