dfed / SafeDI

Compile-time safe dependency injection in Swift

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Using an Instantiator with a protocol causes a build failure

MrAdamBoyd opened this issue · comments

I am using the example project integration in the codebase.

  1. Open NoteView.swift
  2. Change any UserService type to Instantiator<any UserService>
  3. Build error: Type 'any UserService' does not conform to protocol 'Instantiable'

Sample code or sample project upload

@MainActor
@Instantiable
public struct NoteView: Instantiable, View {
    public init(userName: String, userServiceBuilder: Instantiator<any UserService>, stringStorage: StringStorage) {
        self.userName = userName
        self.userServiceBuilder = userServiceBuilder
        self.stringStorage = stringStorage
        _note = State(initialValue: stringStorage.string(forKey: userName) ?? "")
    }

    public var body: some View {
        VStack {
            Text("\(userName)’s note")
            TextEditor(text: $note)
                .onChange(of: note) { _, newValue in
                    stringStorage.setString(newValue, forKey: userName)
                }
            Button(action: {
                userService.userName = nil
            }, label: {
                Text("Log out")
            })
        }
        .padding()
    }

    @Forwarded
    private let userName: String
    @Instantiated
    private let userServiceBuilder: Instantiator<any UserService>
    @Received
    private let stringStorage: StringStorage
    private lazy var userService = userServiceBuilder.instantiate()

    @State
    private var note: String = ""
}

#Preview {
    NoteView(
        userName: "dfed",
        userService: DefaultUserService(stringStorage: UserDefaults.standard),
        stringStorage: UserDefaults.standard
    )
}

Yeah, that makes sense, and is unfortunate. Today returning existentials (protocols without concrete types) requires utilizing ErasedInstantiator since, well... you are erasing the type.

This is a bit annoying, but there's some good news: SafeDI can rename a property! So you can do something like:

public protocol ErasedType {}
@Instantiable
public final class ConcreteType: ErasedType, Instantiable {
   ... // no forwarded properties here for this example
}

public typealias ErasedTypeInstantiator = ErasedInstantiator<(), ErasedType>

@Instantiable
public final class ParentType: Instantiable {
    @Instantiated(fulfilledByType: "ConcreteType") private let erasedTypeErasedBuilder: ErasedInstantiator<(), ErasedType>
    @Received(fulfilledByDependencyNamed: "erasedTypeErasedBuilder", ofType: ErasedInstantiator<(), ErasedType>.self) private let erasedTypeBuilder: ErasedTypeInstantiator
}

I haven't tested that... but I think it might work.

However, we're kinda working around both SafeDI and the Swift type system with that approach. Our problem here is that we've got an existential without a concrete type. So... let's make a concrete type!

public protocol ErasedType {
    // some API here
}
@Instantiable
public final class ConcreteType: ErasedType, Instantiable {
   ... // no forwarded properties here for this example
}

@Instantiable
public final class AnyErasedType: ErasedType, Instantiable {
    /// An initializer that you can use for your own unit testing.
    public init(_ erasedType: any ErasedType) {
        self.erasedType = erasedType
    }

    /// An initializer that SafeDI uses.
    public init(/* take all DI arguments here */) {
        erasedType = ConcreteType(/* forward your DI arguments here */)
    }

    // ErasedType API here, deferring all calls to `erasedType`'s implementation.

    private let erasedType: any ErasedType
}

@Instantiable
public final class ParentType: Instantiable {
    @Instantiated private let erasedTypeBuilder: Instantiator<AnyErasedType>
}

And now you have a concrete existential, so it compiles! Type erasure to the rescue!

Let me know which approach you like. Maybe we can take a task to document this better in the README. But for now, I'm gonna close this out since we have a few options on the table.