hmlongco / Resolver

Swift Ultralight Dependency Injection / Service Locator framework
MIT License
2.15k stars 190 forks source link

@InjectedStateObject feature request with potential solution #110

Closed hydro1337x closed 3 years ago

hydro1337x commented 3 years ago

I started using Resolver in combination with SwiftUI and noticed that there is no way to inject an @StateObject property. I used the @InjectedObject wrapper but the problem with it or better to say with ObservedObjects is that they do not store the state if the view in which they are declared redraws itself.

Potential solution

@available(OSX 10.15, iOS 14.0, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct InjectedStateObject<Service>: DynamicProperty where Service: ObservableObject {
    @StateObject private var service: Service
    public init() {
        self._service = StateObject(wrappedValue: Resolver.resolve(Service.self))
    }
    public init(name: Resolver.Name? = nil, container: Resolver? = nil) {
        self._service = StateObject(wrappedValue: container?.resolve(Service.self, name: name) ?? Resolver.resolve(Service.self, name: name))
    }
    public var wrappedValue: Service {
        get { return service }
    }
    public var projectedValue: ObservedObject<Service>.Wrapper {
        return self.$service
    }
}

Demo project

import SwiftUI
import Resolver

class DataSource: ObservableObject {
    @Published var counter = 0
}

struct Counter: View {
    @InjectedStateObject var dataSource: DataSource

    var body: some View {
        VStack {
            Button("Increment counter") {
                dataSource.counter += 1
            }

            Text("Count is \(dataSource.counter)")
        }
    }
}

struct ItemList: View {
    @State private var items = ["hello", "world"]

    var body: some View {
        VStack {
            Button("Append item to list") {
                items.append("test")
            }

            List(items, id: \.self) { name in
                Text(name)
            }

            Counter()
        }
    }
}
hmlongco commented 3 years ago

If you use @InjectedObject your registrations need to be scoped, usually as "shared".

register { DataSource() }.scope(.shared)

This will let DataSource persist for as long as there's a Counter view in the view graph.