ivlevAstef / DITranquillity

Dependency injection for iOS (Swift)
MIT License
421 stars 32 forks source link

Custom scopes #108

Closed stanmots closed 6 years ago

stanmots commented 6 years ago

Hi,

Thank you for the library! It's nice to have a decent DI-library for pure Swift projects.

Could you please clarify a couple of things for me. I'm trying to wrap my head around custom scope objects. Let's say I have three VCs: VC1 -> VC2 -> VC3. And I want to share some object between them.

I understand, that I can just register the object as single (.single, .lazySingle or .weakSingle). But it will live so long as the whole app. In my case I want to bind its lifecycle to the aforementioned view controllers.

What is the best way to achieve this? Should I create another DIContainer just for these VCs?

ivlevAstef commented 6 years ago

Hello.

Yes you can make more DIContainers - DIPart allows you to do this without duplicating code. But weakSingle will live while there are references to the object. In your example if you destroy VC1, VC2, VC3 then is removed object.

Or you can use objectGraph, but it will only fit if all VC are generated together.

I think in your case probably it is better to use weakSingle.

stanmots commented 6 years ago

Thanks for the reply,

But, suppose we have configured a global container

let globalContainer = DIContainer()

and a container as per my use case

let useCaseContainer = DIContainer()

Now to inject dependencies and present a view controller we must create an appropriate storyboard:

let storyboard = DIStoryboard.create(name: "Main", bundle: nil, container: container)

Here is the dilemma: what container should we pass to the storyboard factory method? What if we need the singleton objects from globalContainer to be available in useCaseContainer?

I see that DIPart helps in reducing code duplications. However, if understand it correctly, each container will create its own instances of the objects which were configured as singles. So, is there a way to share these instances between containers?

ivlevAstef commented 6 years ago

You can do this:

class Part: DIPart {
  static func load(container: DIContainer) {
    container.registerStoryboard(name: "Main")
      .lifetime(.lazySingle)
  }
}

let globalContainer = DIContainer()
let useCaseContainer = DIContainer()
globalContainer.append(Part.self)
useCaseContainer.append(Part.self)

let storyboard1: UIStoryboard = *globalContainer
let storyboard2: UIStoryboard = *useCaseContainer
/// storyboard1 === storyboard2 - true

But there is a problem, a new ViewController (and all its dependencies too) will be created from the container which was the first. In example globalContainer.

Unfortunately, DI cannot solve all problems :) But if you've seen a solution to your problem in the other DI then tell me where, and maybe I'll add to your library. The main requirement is that the solution should be universal.

Perhaps you would have helped scope of autofac: http://autofac.readthedocs.io/en/latest/lifetime/working-with-scopes.html Most likely I will return them but later when I think of how they successfully make in swift. They rarely need.

Or: https://github.com/ivlevAstef/DITranquillity/issues/83 - It is a universal solution for the most challenging cases. Now in a priority to do it.

ivlevAstef commented 6 years ago

Also, you can use a storyboardReference - that is, your VC1,VC2,VC3 can be on one storyboard, and the other VC to the other storyboards. Then, this storyboard may belong to a separate container.

ivlevAstef commented 6 years ago

A small cheat:

let globalContainer = DIContainer()
let useCaseContainer = DIContainer()

globalContainer.registerStoryboard(name: "Main")
    .lifetime(.lazySingle)

globalContainer.register(VC1.self)
    .postInit { $0.object = *useCaseContainer }

globalContainer.register(VC2.self)
    .postInit { $0.object = *useCaseContainer }

let yourObject = YourObject()
useCaseContainer.register{ yourObject }

But I do not welcome such a solution.

But I realized that I should add time of life perContainer: https://github.com/ivlevAstef/DITranquillity/issues/109

ivlevAstef commented 6 years ago

From 3.1.2 old case can rewrite on:

let globalContainer = DIContainer()
let useCaseContainer = DIContainer()

globalContainer.registerStoryboard(name: "Main")
    .lifetime(.lazySingle)

globalContainer.register(VC1.self)
    .postInit { $0.object = *useCaseContainer }

globalContainer.register(VC2.self)
    .postInit { $0.object = *useCaseContainer }

useCaseContainer.register(YourObject.init)
  .lifetime(.perContainer)

But you have to understand when the container can be destroyed or recreated.

stanmots commented 6 years ago

Wow, so quick support 👍

Yeah, the perContainer scope is a nice addition.

I'm using Dagger 2 for dependency injection in Android. I like their concept of Components and Subcomponents. That's why I was looking for a similar functionality in the DI lib for Swift.

As stated in the docs, there are two main advantages of using subcomponents:

  1. Custom scopes. While the root component holds all the singletons the children subcomponents can be created/destroyed any time we want. Basically, it is our responsibility to determine lifetime of the subcomponents.

I think, in Swift we can achieve a similar behaviour by using weakSingle scope as you advised in previous comments.

  1. Encapsulation. E.g. when two screens have shared objects plus some other objects that should be unique to a particular screen. Subcomponents make it possible to ensure these unique objects won't be accessed by the wrong screen controller (Activity in Android or UIViewController in iOS).

Okay, I'll play with perContainer to see how the architecture can be improved. I'm closing the issue for now.