Property wrappers + dependency injection: why do I need a setter here?

Hi,

I was playing around with a simple Dependency Injection / Service Locator setup, but couldn't quite understand why the following code only compiles when Inject.wrappedValue.set is enabled?

At first, I didn't see a need to store the injected value in the property, so the original implementation of the getter was just reaching into the DependencyResolvers dictionary. Enabling the setter made me wonder if I should write through to the dictionary again, which seemed totally wrong. I then added the "dependency" member to the property wrapper, but that also seems unnecessary, since wrappedValue could be a computed property?

With my current knowledge, I would have expected that the flag mutation should be fine on the target instance, since the protocol specifies mutability via "set", and the instance itself is a refcounted class, not a value type.

A setter does make sense for mutating properties of value types, as far as I understand.

Does the compiler lose type information, and at which step? Is it a simplification in favor of value types? I hope someone can shed some light on this.

Thanks, and great forum!


public class DependencyResolver {

    public static let shared = DependencyResolver()

    private var dependencies: [String: Any] = [:]

    public func register<T>(_ instance: T) {
        let name = String(describing: T.self)
        if dependencies.keys.contains(name) {
            fatalError("registering instance für '\(name)' would overwrite previously registered instance. You can register each type only once.")
        } else {
            print("registering \(name)")
            dependencies[name] = instance
        }
    }

    public func resolve<T>() -> T {
        let name = String(describing: T.self)
        if dependencies.keys.contains(name) {
            return dependencies[name] as! T
        } else {
            fatalError("couldn't find instance for type '\(name)'")
        }
    }
}

@propertyWrapper
public struct Inject<T> {
    private var dependency: T

    public init() {
        dependency = DependencyResolver.shared.resolve()
    }

    public var wrappedValue: T {
        get { dependency }
        // set { dependency = newValue }
    }
}

protocol Settings {
    var flag: Bool { get set }
}

class SettingsDatasource: Settings {
    var flag: Bool = true
}

let settingsDS = SettingsDatasource()

DependencyResolver.shared.register(settingsDS as Settings)

func test() {
    @Inject var settings: Settings

    settings.flag = false

}


1 Like

I think it's because you're storing an existential (aka protocol type), Settings (which needs to be spelled any Settings in Swift 6 FYI, and you can and should adopt this spelling today), and existentials are value types?

If you remove the as Settings and change the settings property to be SettingsDatasource then it seems to work. Perhaps you could use a super class instead of a protocol.

Or you could try to make it work with keypaths like SwiftUI's @Environment, or a similar solution that was recently shared here: Dictionary with types as keys and different type values? - #13 by Jon_Shier

1 Like