Nickersoft
Nickersoft

Reputation: 684

Is it possible to nest property wrappers in Swift when using @ObservedObject?

I have recently started to dig into the wonderful world of SwiftUI, Combine, and property wrappers and am struggling to combine @ObservedObject with the @Injected property wrapper I wrote to inject dependencies into my views. Most of the time my @Injected wrapper works fine, but when paired with @ObservedObject to manage my viewmodels I received "Property type does not match that of the 'wrappedValue' property" errors."

Here's currently what my @Injected property wrapper looks like:

@propertyWrapper
public struct Injected<Service> {

    private var service: Service

    public init() {
        self.service = assembler.resolver.resolve(Service.self)!
    }

    public init(name: String? = nil, container: Resolver? = nil) {
        // `assembler` here referring to my global Swinject assembler
        self.service = container?.resolve(Service.self, name: name) ??
            assembler.resolver.resolve(Service.self, name: name)!
    }

    public var wrappedValue: Service {
        get {
            return service
        }

        mutating set {
            service = newValue
        }
    }

    public var projectedValue: Injected<Service> {
        get {
            return self
        }

        mutating set {
            self = newValue
        }
    }
}

And here is my current usage:

struct MyModalView: View {
    
    @ObservedObject @Injected var viewModel: MyModalViewModel
    
    var body: some View {
        Text("Hello World")
    }
}

Ordering the wrappers in this way, I receive: "Property type 'MyModalViewModel' does not match that of the 'wrappedValue' property of its wrapper type 'ObservedObject'", while the MyModalViewModel class does extend from ObservableObject.

If I flip the wrappers around, it compiles, but Swinject tries to resolve a wrapped ObservedObject class, and because the container is just registering the original MyModalViewModel class, this resolution fails and the app crashes.

Meanwhile, assigning the @ObservedObject value through direct assignment works:

@ObservedObject var viewModel: MyModalViewModel = assembler.resolver.resolve(MyModalViewModel.self)!

I would think the original code should compile, seeing @Injected will return a wrapped value that conforms to ObservableObject, like @ObservedObject expects, though all of this is still fairly new to me so there might be something I'm missing. Any input here would be greatly appreciated. Thanks!!

Upvotes: 2

Views: 1552

Answers (2)

sgelves
sgelves

Reputation: 134

Use @InjectedObject annotation, or Service locator from Resolver. And, don't forget to register your initializer.

A tutorial here

Upvotes: 0

Nickersoft
Nickersoft

Reputation: 684

For anyone coming upon this in the future, I wound up combining the two property wrappers under one roof, borrowing the implementation from the Resolver project:

@propertyWrapper
public struct InjectedObject<Service>: DynamicProperty where Service: ObservableObject {
 
    @ObservedObject private var service: Service
    
    public init() {
        self.service = assembler.resolver.resolve(Service.self)!
    }
    
    public init(name: String? = nil, container: Resolver? = nil) {
        self.service = container?.resolve(Service.self, name: name) ??
            assembler.resolver.resolve(Service.self, name: name)!
    }
    
    public var wrappedValue: Service {
        get { return service }
        mutating set { service = newValue }
    }
    
    public var projectedValue: ObservedObject<Service>.Wrapper {
        return self.$service
    }
}

Upvotes: 5

Related Questions