简体   繁体   中英

SwiftUI optional environment object

I'm using @EnvironmentObject like this:

struct MyView: View {
  @EnvironmentObject var object: MyObject

  ...
}

but my code doesn't need there to be a value for 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' )

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 . ei these don't work:

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

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 .

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<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. Additionally, you can also leverage EnvironmentValues to access the object via key path based 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.

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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