square / Cleanse

Lightweight Swift Dependency Injection Framework
Other
1.78k stars 90 forks source link

Inject a single property into multiple objects #174

Closed dbtech closed 3 years ago

dbtech commented 3 years ago

Actually I want to rephrase a question that had already been asked over 3 years ago and closed 2 years later (see issue #45). One of my reasons to return to the subject is the possibility that the Components in the current version might be essentially different from the earlier one.

I am about to port an Android app relying, among others, on two important components: Dagger 2 and Android ViewModel. One of the strongest motivations for me to try Cleanse is the chance of using the same or similar concepts found in Dagger. For this reason Property Injection (PI) is indispensable in ViewControllers.

As far as I understand, PI into a specific object of a class A is only supported by a Cleanse Component C[A] bound hard-coded to that class A. If that is the case then PI into n classes would require n components even if all those classes need only a single element of the dependency graph to be injected.

Could someone confirm the one-to-one limitation in Cleanse? Are there any workarounds? How do you inject a single property into multiple objects?

sebastianv1 commented 3 years ago

As far as I understand, PI into a specific object of a class A is only supported by a Cleanse Component C[A] bound hard-coded to that class A.

I don't quite follow what you mean by this. Maybe a code sample would help demonstrate? There isn't a one-to-one coupling between property injection and components. Components (or scopes) hold all the bindings for your graph similar to Dagger, and you can create any number of property injectors in each component. For example, let's say we had two View Controllers (ViewControllerA and ViewControllerB) that both use property injection and share a dependency on MyAppService.

struct AppComponent: Cleanse.Component {
    // ...
    static func configure(binder: Binder<Unscoped>) {
        binder
            .bind(MyAppService.self)
            .to(factory: MyAppService.init)

        binder
            .bindPropertyInjectionOf(ViewControllerA.self)
            .to { (targetA: ViewControllerA, service: MyAppService) in
                targetA.service = service
            }

        binder
            .bindPropertyInjectionOf(ViewControllerB.self)
            .to { (targetB: ViewControllerB, service: MyAppService) in
                targetB.service = service
            }
    }
    // ...
}

And then you can inject instances of PropertyInjector<ViewControllerA> and PropertyInjector<ViewControllerB> into a root object for instance and construct them. Although, generally PI is considered a second-class API and prefer using constructor injection or assisted injection if your view controllers require "just in time" dependencies. Ideally we should only use PI if we can't control construction of the object in cases like Storyboards or the AppDelegate.

Hopefully this helps?

dbtech commented 3 years ago

Yes! Thank you for the fast and comprehensive answer. It was really swift :). The closure in ".to { }" seems straightforward, however I was not able to find the solution on my own. The above sample might deserve to be included in the README.

sebastianv1 commented 3 years ago

Good idea! Do you mind elaborating on which part was confusing and would help explaining in the README? The property injector portion, or using the to { ... } factory closure?

dbtech commented 3 years ago

It was the factory closure that helped. Yet I am not sure if that is the right answer to your question. I have read the Property Injection section thoroughly but I was unable to translate the generic explanation into specific code. It is clearly explained that there are two types of the terminating function but due to my limited swift skills I could not match any of them to the following code example which illustrates only one of them. The code sample for the other, "simple" first case helped.