GithubHelp home page GithubHelp logo

dfed / safedi Goto Github PK

View Code? Open in Web Editor NEW
76.0 76.0 4.0 723 KB

Compile-time safe dependency injection in Swift

License: MIT License

Swift 99.87% Shell 0.13%
dependency-injection dependency-management swift

safedi's People

Contributors

bachand avatar dfed avatar mradamboyd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

safedi's Issues

Let Instantiables conform to `Instantiable` by default

Objectives
Enhance the ease of declaring instantiators.

Discussion
With the recent update to SafeDI 0.5.0, to create an insatiable builder, users need to perform two steps:

  1. Apply the @Instantiable macro to the desired concrete type.
  2. Ensure that the concrete type conforms to Instantiable.

Proposal
Could SafeDI automatically generate the conformance using macro code generation, eliminating the need for users to manually add it?

@Instantiable
public class ConcreteType {
    ...
}

Generated Code

extension ConcreteType: Instantiable { }

Fixit for adding public to a class removes `final`

Steps to reproduce

  1. Create new class and declare it final
  2. Mark it as instantiate with SafeDI
  3. Error appears: Instantiable-decorated type must be public or open
  4. Apply fixit, class declaration is now public class XYZ instead of public final class XYZ

Expected behavior
Fixit applies public class

Actual behavior
Fixit applies public final class

Enable a dependency root to @Received(fulfilledByDependencyNamed:ofType:) an @Instantiated property

Goals

  • Share a single instance as multiple properties with different names or types from a root object
  • Continue to ensure at plugin runtime that received properties are instantiated prior to being received

Non-Goals

  • Ensure at plugin runtime that the new type of the received property can be fulfilled by the desired dependency (i.e. do not change how @Received(fulfilledByDependencyNamed:ofType:) works today).

Investigations
This can be accomplished today by utilizing a @Instantiated extension whose instantiate() method always returns the same instance for multiple types, but this is a limiting way to accomplish the above goals since it ensures that we'll only ever have a single instance, which might not be what we want.

Right now, the only thing preventing roots from having an aliased received property is our own code that identifies roots.

Design
The design here would enable a root object to declare a @Received(fulfilledByDependencyNamed:ofType:) for a property that is @Instantiated on the same type. This will require updating DependencyTreeGenerator's lazy var possibleRootInstantiableTypes implementation, or alternatively updating the name and implementation of fileprivate var areAllInstantiated: Bool.

One thing to consider is that each of a possible root type's @Received(fulfilledByDependencyNamed:ofType:) properties must refer to @Instantiated property declared on that same Instantiable struct. Doing this check performantly will require creating a Set of an Instantiable's dependencies. We can likely create this set just-in-time and throw it away, since we don't have another use for this Set just yet, and other code currently relies on the ordering of public let dependencies: [Dependency] on Instantiable.

The PR that implements this issue should have the following tests added to SafeDIToolTests:

A test to ensure that we identify a root with an aliased property
func test_run_writesConvenienceExtensionOnRootOfTree_whenRootHasReceivedAliasOfInstantiable() async throws {
    let output = try await executeSystemUnderTest(
        swiftFileContent: [
            """
            @Instantiable
            public struct Root {
                @Instantiated
                private let defaultUserService: DefaultUserService

                @Received(fulfilledByDependencyNamed: "defaultUserService", ofType: DefaultUserService.self)
                private let userService: any UserService
            }
            """,
            """
            import Foundation

            public protocol UserService {
                var userName: String? { get set }
            }

            @Instantiable(fulfillingAdditionalTypes: [UserService.self])
            public final class DefaultUserService: UserService {
                public init() {}

                public var userName: String?
            }
            """,
        ],
        buildDependencyTreeOutput: true
    )

    XCTAssertEqual(
        try XCTUnwrap(output.dependencyTree),
        """
        // This file was generated by the SafeDIGenerateDependencyTree build tool plugin.
        // Any modifications made to this file will be overwritten on subsequent builds.
        // Please refrain from editing this file directly.

        #if canImport(Foundation)
        import Foundation
        #endif

        extension Root {
            public init() {
                let defaultUserService: DefaultUserService = DefaultUserService()
                let userService: any UserService = defaultUserService
                self.init(defaultUserService: defaultUserService, userService: userService)
            }
        }
        """
    )
}
A test to ensure that we do not identify an instantiable with an aliased property as a root if that aliased property is not found on this type
func test_run_successfullyGeneratesOutputFileWhenNoRootFound() async throws {
    let output = try await executeSystemUnderTest(
        swiftFileContent: [
            """
            @Instantiable
            public struct NotRoot {
                @Instantiated
                private let defaultUserService: DefaultUserService

                // This received property's alias is improperly configured, meaning that this type is not a root.
                @Received(fulfilledByDependencyNamed: "userService", ofType: DefaultUserService.self)
                private let userService: any UserService
            }
            """,
            """
            import Foundation

            public protocol UserService {
                var userName: String? { get set }
            }

            @Instantiable(fulfillingAdditionalTypes: [UserService.self])
            public final class DefaultUserService: UserService {
                public init() {}

                public var userName: String?
            }
            """,
        ],
        buildDependencyTreeOutput: true
    )

    XCTAssertEqual(
        try XCTUnwrap(output.dependencyTree),
        """
        // This file was generated by the SafeDIGenerateDependencyTree build tool plugin.
        // Any modifications made to this file will be overwritten on subsequent builds.
        // Please refrain from editing this file directly.

        // No root @Instantiable-decorated types found, or root types already had a `public init()` method.
        """
    )
}

This PR should also include an update to the README to mention this capability! This may be the toughest part of the task.

Enable generic types to be instantiated

Today, a Container<Element> type cannot effectively be @Instantiable or @Instantiated unless an init method can be written that does not utilize the generic Element

Ideally, it would be possible for a type with a generic to be @Instantiable. Imagine something like:

@Instantiable
extension Container: Instantiable {
    public static func instantiate() -> Container<Bool> {
        .init(contained: false)
    }

    public static func instantiate() -> Container<Int> {
        .init(contained: 0)
    }

    public static func instantiate() -> Container<String> {
        .init(contained: "")
    }
}

Accomplishing this should be relatively straight-forward. We currently have a FixableInstantiableError.incorrectReturnType that throws whenever the return type's description is not the same as the extended type. We could make this error more permissive and not throw as long as the base name (i.e. the type name without generics) is the same.

I don't see a good way to make generics instantiable outside of an extension, but I am open to ideas!

Enable instantiation cycles

Goals

  • Enable the lazy instantiation of cycles of dependencies. i.e. it should be possible for A to lazily create B which lazily creates C which lazily creates A.

Non-Goals

  • Enable the aggressive instantiation of cycles of dependencies. i.e. it should continue to not be possible for A to instantiate B which instantiates C which instantiates A.
  • Enable the receiving of cycles of dependencies. i.e. it should continue to not be possible for A to receive B which receives C which receives A.

Design
Our generated code today has a fundamental flaw which does not enable the lazy instantiation of cycles of dependencies: properties that fulfill lazy instantiation can not be called from within their own definition. Here's how a type A which lazily creates B which lazily creates C which lazily creates A fails to compile with our current code generation:

extension Root {
    public convenience init() {
        let aBuilder = Instantiator<A> {
            let bBuilder = Instantiator<B> {
                let cBuilder = Instantiator<C> {
                    C(aBuilder: aBuilder) // Fails to compile! We can't reference aBuilder from within aBuilder's definition.
                }
                return B(cBuilder: cBuilder)
            }
            return A(bBuilder: bBuilder)
        }
        self.init(aBuilder: aBuilder)
    }
}

To enable the lazy creation of dependency cycles, we need a way to reference a builder from within the builder's definition. While a property can't be referenced from within a closure that instantiates the property, a function can. Here's how a type A which lazily creates B which lazily creates C which lazily creates A could be written in a way that compiles:

extension Root {
    public convenience init() {
        func __safeDI_aBuilder() -> A {
            func __safeDI_bBuilder() -> B {
                func __safeDI_cBuilder() -> C {
                    let aBuilder = Instantiator<A>(__safeDI_aBuilder)
                    return C(aBuilder: aBuilder)
                }
                let cBuilder = Instantiator<C>(__safeDI_cBuilder)
                return B(cBuilder: cBuilder)
            }
            let bBuilder = Instantiator<B>(__safeDI_bBuilder)
            return A(bBuilder: bBuilder)
        }
        let aBuilder = Instantiator<A>(__safeDI_aBuilder)
        self.init(aBuilder: aBuilder)
    }
}

When ScopeGenerator‘s func generateCode encounters an instantiator or forwarding instantiator property type, we should generate the builder method, and then declare the property that calls into the defined builder method. When a property declares a builder that forms the re-entry into the cycle, we should declare the property that calls into the builder method immediately, knowing that the call will compile + recurse.

Functions to fulfill forwarding instantiators will declare arguments matching the forwarded properties.

Where we detect and throw ScopeError.dependencyCycleDetected today, we'll instead need to create and return a ScopeGenerator that is aware that the builder function it needs to re-enter the cycle is already defined.

Generated initializers should decorate closure types with `@escaping`

Steps to reproduce
Write the following type:

@Instantiable
public struct ClosureContainer {
    @Forwarded
    let closure: () -> Void
}

Expected behavior
SafeDI would generate an initializer:

public init(closure: @escaping () -> Void) {
    self.closure = closure
}

Actual behavior
SafeDI generates an initializer that does not compile because the closure is not escaping:

public init(closure: () -> Void) {
    self.closure = closure
}

Worse yet, SafeDI generates this initializer even if you have written the following initializer:

public init(closure: @escaping () -> Void) {
    self.closure = closure
}

This means it is not possible to have a @Forwarded closure work with SafeDI today

Offending code lives in Initializer.generateRequiredInitializer. A PR addressing this issue should add the following test:

func test_declaration_generatesRequiredInitializerWithClosureDependency() {
    assertMacro {
        """
        @Instantiable
        public struct ExampleService {
            @Forwarded
            let closure: () -> Void
        }
        """
    } expansion: {
        """
        public struct ExampleService {
            let closure: () -> Void

            // A generated initializer that has one argument per SafeDI-injected property.
            // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros.
            // As a result, this initializer can not be used within a #Preview macro closure.
            // This initializer is only generated because you have not written this macro yourself.
            // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros.
            nonisolated public init(closure: @escaping () -> Void) {
                self.closure = closure
            }
        }
        """
    }
}

Provide a better error when type names are not equally-qualified in instantiated and received declarations

Goals
Make it less painful to debug when you refer to a type in an Instantiator by two different names.

Design
Give a more actionable error when we have a type that doesn't match anything, but there's an existing type that is the same name prefixed by another type. For example:

@Instantiated private let someThing: Instantiated<View>

@Received private let someThing: Instantiated<MyViewController.View>

ForwardingInstantiator typealias for easier declarations

Goals
Take an example class:

@Instantiable
public final class MetricsService {
  @Forwarded
  private let user
}

If a class wants to instantiate MetricsService, they must use this declaration:

@Instantiated
private let metricsServiceBuilder: ForwardingInstantiator<MetricsService.ForwardedArguments, MetricsService>

However, SafeDI can easily generate another typealias, similar to forwardedArguments which makes this declaration shorter:

@Instantiated
private let metricsServiceBuilder: MetricsService.ForwardingInstantiator

Which would look like this in the codegen:

extension MetricsService {
  typealias ForwardingInstantiator = FowardingInstantiator</* forwarded types here */, MetricsService> 
}

nonisloated init produces warning on classes decorated with @MainActor

Steps to reproduce

  1. Upgrade to Xcode 15.3
  2. Try to build

Sample code or sample project upload

@MainActor
@Instantiable
public final class ApplicationCoordinator: NSObject {
    private(set) var window: UIWindow?

    // MARK: - Initializers
    nonisolated
    public init(
        featureFlagManager: FeatureFlagManager
    ) {
        self.featureFlagManager = featureFlagManager
        super.init()
        // Do other work here
    }

    @Instantiated
    private let featureFlagManager: FeatureFlagManager

Now results in an error:
Main actor-isolated property featureFlagManager can not be mutated from a non-isolated context; this is an error in Swift 6

Improve error message when receiving a property that forms a cycle

Today, we have good dependency cycle error messages when there's an @Instantiated cycle, but our error message when a cycle involves a @Received property could use some work. Today, the following test fails:

    func test_run_onCodeWithCircularPropertyDependenciesImmediatelyInitializedAndReceived_throwsError() async {
        await assertThrowsError(
            """
            Dependency cycle detected!
            A -> B -> C -> A
            """
        ) {
            try await executeSafeDIToolTest(
                swiftFileContent: [
                    """
                    @Instantiable
                    public final class Root {
                        @Instantiated let a: A
                    }
                    """,
                    """
                    @Instantiable
                    public final class A {
                        @Instantiated let b: B
                    }
                    """,
                    """
                    @Instantiable
                    public final class B {
                        @Instantiated let c: C
                    }
                    """,
                    """
                    @Instantiable
                    public final class C {
                        @Received let a: A
                    }
                    """,
                ],
                buildDependencyTreeOutput: true,
                filesToDelete: &filesToDelete
            )
        }
    }

Because it produces error message:

@Received property `a: A` is not @Instantiated or @Forwarded in chain: Root -> A -> B -> C

We should instead make this test pass.

Enable receiving builders in a cycle

Today, if you have builders (Instantiator<...> types) that form a cycle, you must @Instantiate the cycle'd builder rather than @Receive-ing it. If you try to @Receive the cycle builder, you get an error that the builder doesn't exist in the chain. This is technically incorrect – it does exist in the chain, but it's inaccessible in the code-gen due to the cycle.

To address this technical deficiency, we should enable this test to pass:

    func test_run_writesConvenienceExtensionOnRootOfTree_whenLazyInstantiationCycleExistsAndCycleBuilderIsReceived() async throws {
        let output = try await executeSafeDIToolTest(
            swiftFileContent: [
                """
                @Instantiable
                public struct Root {
                    @Instantiated let aBuilder: Instantiator<A>
                }
                """,
                """
                @Instantiable
                public struct A {
                    @Instantiated let bBuilder: Instantiator<B>
                }
                """,
                """
                @Instantiable
                public struct B {
                    @Instantiated let cBuilder: Instantiator<C>
                }
                """,
                """
                @Instantiable
                public struct C {
                    @Received let aBuilder: Instantiator<A>
                }
                """,
            ],
            buildDependencyTreeOutput: true,
            filesToDelete: &filesToDelete
        )

        XCTAssertEqual(
            try XCTUnwrap(output.dependencyTree),
            """
            // This file was generated by the SafeDIGenerateDependencyTree build tool plugin.
            // Any modifications made to this file will be overwritten on subsequent builds.
            // Please refrain from editing this file directly.

            extension Root {
                public init() {
                    func __safeDI_aBuilder() -> A {
                        func __safeDI_bBuilder() -> B {
                            func __safeDI_cBuilder() -> C {
                                let aBuilder = Instantiator<A>(__safeDI_aBuilder)
                                return C(aBuilder: aBuilder)
                            }
                            let cBuilder = Instantiator<C>(__safeDI_cBuilder)
                            return B(cBuilder: cBuilder)
                        }
                        let bBuilder = Instantiator<B>(__safeDI_bBuilder)
                        return A(bBuilder: bBuilder)
                    }
                    let aBuilder = Instantiator<A>(__safeDI_aBuilder)
                    self.init(aBuilder: aBuilder, notCyclic: notCyclic)
                }
            }
            """
        )
    }

Not all generated initializers on classes or structs should include the `nonisolated` keyword

Steps to reproduce
Write the following type:

@Instantiable
public struct ExampleService {}

Expected behavior
The following initializer would be generated:

public init() {}

Actual behavior
The following initializer is generated:

nonisolated public init() {}

Ideally, the initializer would be marked as nonisolated only if the type is decorated with @MainActor. The offending code lives in ConcreteDeclType.initializerModifiers. When this issue is addressed, I would expect a test similar (or identical) to the below to be included in the PR:

func test_declaration_generatesRequiredInitializerOnMainActorBoundClass() {
    assertMacro {
        """
        @MainActor
        @Instantiable
        public class ExampleService {
        }
        """
    } expansion: {
        """
        @MainActor
        public class ExampleService {

            // A generated initializer that has one argument per SafeDI-injected property.
            // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros.
            // As a result, this initializer can not be used within a #Preview macro closure.
            // This initializer is only generated because you have not written this macro yourself.
            // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros.
            nonisolated public init() {
            }
        }
        """
    }
}

I would also expect test_declaration_generatesRequiredInitializerWithoutAnyDependenciesOnClass() (and possibly a few others) to be updated to remove the nonisolated keyword from generated code.

Note that this issue does not lead to compilation errors. Addressing this issue is an enhancement rather than a bugfix.

Support type erasure of SwiftUI View with AnyView

Goals

  • Enable dependency inversion when utilizing SwiftUI, like we do with UIKit
  • Enable wrapping an @Instantiated property that is fulfilled by another type in the declared type-erased type's initializer when appropriate
  • Enable wrapping a renamed @Received property in the declared type-erased type's initializer when appropriate
  • Avoid adding more complexity than necessary

Non-Goals

  • Prevent consumers from writing code that will cause code-generation to fail to compile (we don't have enough type information to accomplish this)

Investigations

Today, we support type erasure with UIKit by enabling a property to return a type further up in the inheritance tree than the object being created. e.g. it is possible to have a Instantiator<UIViewController> when we are actually creating a subclass of UIViewController

However, because SwiftUI both does not have an inheritance tree and has generics, a Instantiator<View> fails to compile. Instantiator<any View> can compile, but is useless since an AnyView can't take an any View in its initializer, and therefore AnyView(myAnyViewInstantiator.instantiate()) fails to compile. Similarly, Instantiator<some View> fails to compile because there is no initialization clause.

It is standard practice in Swift libraries for an Any* type to have a no-label initializer. However, AnyActor is an exception to this standard practice.

Initial design concept

The initial proposed design is simple: any @Instantiated(fulfilledByType:) or @Received(fulfilledByDependencyNamed:ofType:) property where the returned instance is a type that has an Any prefix will have the generated code that fulfills that type wrapped in the AnyType's initializer.

In the following example, a type-erased noteViewBuilder

@Instantiated(fulfilledByType: "NoteView")
private let noteViewBuilder: ForwardingInstantiator<String, AnyView>

Would have the following code generated for it:

func __safeDI_noteViewBuilder(userName: String) -> AnyView {
    AnyView(NoteView(userName: userName, userService: userService, stringStorage: stringStorage))
}

We would know to do this because the concrete returned type of the forwarding instantiator is of type AnyView, which has the prefix Any. We also know that there is no @Instantiatiable type AnyView anywhere in the codebase.

While this design makes the strong assumption that any Any-prefixed type that is not @Instantiable is a type eraser with a no-label public init(_:) method, this is true of every Apple-made Any-prefixed type outside of AnyActor. Since making a strong assumption that is already proven incorrect is probably not the best approach, we move onto the next idea.

Second design concept

If we want to avoid making a strong assumption, we can instead create two new macros:

public macro Instantiated(fulfilledByErasedType concreteTypeName: StaticString)
public macro Received<T>(fulfilledByDependencyNamed: StaticString, ofErasedType: T.Type)

When these new ErasedType macros are used, we know that we need to wrap the returned instance in our code generation in a no-label initializer of the erasing type.

In the following example, a type-erased noteViewBuilder

@Instantiated(fulfilledByErasedType: "NoteView")
private let noteViewBuilder: ForwardingInstantiator<String, AnyView>

Would have the following code generated for it:

func __safeDI_noteViewBuilder(userName: String) -> AnyView {
    AnyView(NoteView(userName: userName, userService: userService, stringStorage: stringStorage))
}

This design is more flexible than the initial design, but has the downside of adding yet another complexity layer to every autocomplete where it may not be necessary. I also don't love the ErasedType substring in the argument list.

Third design concept

We can reduce the naming and autocomplete complexity in the above option by amending our existing macros to enable this new functionality:

public macro Instantiated(fulfilledByType concreteTypeName: StaticString, erasedToConcreteExistential: Bool = false)
public macro Received<T>(fulfilledByDependencyNamed: StaticString, ofType: T.Type, erasedToConcreteExistential: Bool = false)

When the new erasedToConcreteExistential macro parameters are used, we know that we need to wrap the returned instance in our code generation in a no-label initializer of the concrete erasing existential type.

In the following example, a type-erased noteViewBuilder

@Instantiated(fulfilledByType: "NoteView", erasedToConcreteExistential: true)
private let noteViewBuilder: ForwardingInstantiator<String, AnyView>

Would have the following code generated for it:

func __safeDI_noteViewBuilder(userName: String) -> AnyView {
    AnyView(NoteView(userName: userName, userService: userService, stringStorage: stringStorage))
}

This option has the benefits of both being explicit and having a sensible default option. Having a default option reduces complexity in the common case, which seems beneficial.

Design

I am strongly leaning towards the third design concept.

Note

When we make this change, we should update the README to use this rather than the non-compiling some View

Add FixIt when decorating a type declaration with Instantiated

@Instantiable and @Instantiated have long common prefixes. It's extremely easy to type the wrong one.

Today, if a type declaration is decorated with @Instantiated, we throw an error. Instead, we should throw an error with a fix it: "This macro must decorate an instance variable. Did you mean @Instantiable?" where the FixIt replaces the decoration with the likely appropriate one.

Same in the opposite direction. Decorating a property declaration with @Instantiable should have a fix it to change the decorator to @Instantiated.

It should be possible to receive a property of one name/type that is fulfilled by a different property name/type

Goals

  • Enable a single instance to be represented by two different properties with different names/types

Non-Goals

  • Enable SafeDI-level checks to ensure that a received property can be fulfilled by another instantiated or forwarded property

Investigations
It's possible to use @Forwarded to enable propagating a property under a new name and type, but given that you can only have one forwarded property per @Instantiable type, this is a limiting approach.

Problem Statement
Take the following code:

public protocol UserProvider {
    var user: User { get }
}

public protocol UserManager: UserProvider {
    func setUserName(_ userName: String)
}

@Instantiable(fulfillingAdditionalTypes: [UserManager.self])
public final class DefaultUserManager: UserManager {
    ... // implementation here
}

It should be possible in one place in the DI tree to instantiate a let userManager: UserManager and further down the DI tree receive a let userProvider: UserProvider whose instance is the same instance as the userManager's.

Design
I believe the simplest path forward here is to add an optional parameter to the @Received macro. Something like:

@Received(fulfilledByDependencyNamed: "userManager")
let userProvider: UserProvider

SafeDI would then know to fulfill the @Received property with an @Instantiated or @Forwarded property further up the tree called userManager. SafeDI would not be able to type check to ensure that the userManager property actually conforms to UserProvider, but if it can't then the generated code would fail to compile in a way that would make the issue fairly obvious.

Note that this design only applies to cases where the received property is of a type the desired dependency already directly conforms to. If UserManager did not inherit from UserProvider, we'd be in trouble. We can add more use-at-your-own-risk flexibility to SafeDI's macros by adding yet another optional parameter to the @Recevied macro:

@Received(fulfilledByDependencyNamed: "userManager", forceCast: true)
let userProvider: UserProvider

This would cause the generated code to force cast the userManager as! UserProvider. I don't expect we'll introduce the forceCast option in the near term, but I outline it here to document how we could solve such a problem down the line.

Generated dependency tree can run afoul of main-actor checking

Steps to reproduce

  1. Make your root object @MainActor
  2. @Instantiate a property of type X in your dependency tree
  3. Make type X @Instantiate a type Instantiator<Y> where Y is @MainActor-bound

Due to what looks like a bug in the Swift Compiler – reported in swiftlang/swift#75003 – the generated code fails to compile with strict concurrency checking turned on.

To resolve this issue, we'll need to avoid utilizing anonymous, just-in-time-created-and-executed closures in our code generation. Instead, we'll need to rely on function declarations which do not exhibit this problem.

The aforementioned Swift issue has been reproduced on both Swift 5.10 and Swift 6 in Swift 6 mode.

Using an Instantiator with a protocol causes a build failure

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.