AliSoftware / Dip

Simple Swift Dependency container. Use protocols to resolve your dependencies and avoid singletons / sharedInstances!
MIT License
978 stars 75 forks source link

Don't know how to really share a .shared scope object #211

Closed Narayane closed 5 years ago

Narayane commented 5 years ago

Hi,

I want to share the same instance of a view model between 2 view controllers. But I don't know how to inject it in them without call resolve() function that "reinit" my view model object instance according to this

Here, part of my Dip container:

self.register(.singleton) { try Repository(graphQL: self.resolve(), fileStorage: self.resolve()) as RepositoryProtocol }
self.register(.shared, tag: "SharedVM") { try SharedViewModel(repository: self.resolve()) as SharedViewModelProtocol }
self.register(storyboardType: AViewController.self, tag: "A")
    .resolvingProperties { container, vc in
        vc.viewModel = try container.resolve(tag: "SharedVM") as SharedViewModelProtocol
    }
self.register(storyboardType: BViewController.self, tag: "B")
    .resolvingProperties { container, vc in
        vc.viewModel = try container.resolve(tag: "SharedVM") as SharedViewModelProtocol
    }

When I load my AViewController, Dip logs:

Trying to resolve AViewController with UI container at index 0
Optional(Context(key: type: AViewController, arguments: (), tag: String("A"), injectedInType: nil, injectedInProperty: nil logErrors: true))
Optional(Context(key: type: SharedViewModel, arguments: (), tag: String("SharedVM"), injectedInType: AViewController, injectedInProperty: nil logErrors: true))
Reusing previously resolved instance Repository
Resolved type SharedViewModelProtocol with SharedViewModel
Resolved type AViewController with <AViewController: 0x157dbcae0>
Resolved AViewController

Then when I push my BViewController from AViewController, Dip logs:

Trying to resolve BViewController with UI container at index 0
Optional(Context(key: type: BViewController, arguments: (), tag: String("B"), injectedInType: nil, injectedInProperty: nil logErrors: true))
Optional(Context(key: type: SharedViewModel, arguments: (), tag: String("SharedVM"), injectedInType: BViewController, injectedInProperty: nil logErrors: true))
Reusing previously resolved instance Repository
Resolved type SharedViewModelProtocol with SharedViewModel // here, I want Reusing previously resolved instance SharedViewModel
Resolved type BViewController with <BViewController: 0x157e58a60>
Resolved BViewController

I don't want to update the view model scope to .singleton because it is not: just a "shared" object for a determinated time

Thanks for helping me

ilyapuchka commented 5 years ago

The difference between shared and singleton scope is that shared only shares instances in the same dependencies graph. That said, after top-most resolve method returns next resolve will produce new instances for shared scope but will reuse previously created instances for singleton scope. So to use shared scope you need to resolve you graph at once, not later at runtime when push happens.

ilyapuchka commented 5 years ago

That said you need to create a graph that will be able to create a second view controller when you push it. You can use factories/builders for that, that you can resolve as part of the dependencies in the beginning and that will have reference to view model that they will pass to view controller that they create lazily at runtime.

Narayane commented 5 years ago
self.register(.singleton) { try Repository(graphQL: self.resolve(), fileStorage: self.resolve()) as RepositoryProtocol }
self.register(type: SharedViewModelProtocol.self, factory: SharedViewModel.init)
self.register(storyboardType: AViewController.self, tag: "A")
    .resolvingProperties { container, vc in
        vc.viewModel = try container.resolve() as SharedViewModelProtocol
    }
self.register(storyboardType: BViewController.self, tag: "B")
    .resolvingProperties { container, vc in
        vc.viewModel = try container.resolve() as SharedViewModelProtocol
    }

Like this? Because same effect than previous code, VM instance is always not shared.

ilyapuchka commented 5 years ago

It's not about registration, it's about how you resolve dependencies for the second view controller. They should be resolved when you resolve the first view controller that will later push the second.

When using storyboard registration it will resolve view controller properties only when it is created from xib, so it will not be the new objects graph.

So with this you either need to use singleton scope or switch from registering second vc with storyboard registration to creating it in code via factory. You can still create it from a storyboard but the factory should already have all the properties this vc needs and should inject them.

class VCFactory {
  let viewModel: SharedViewModelProtocol

  func createSecondVC() -> UIViewController {
    let vc =  Storyboard.instantiateViewController...
    vc.viewModel = viewModel
    return vc
  }
}

register(type: SharedViewModelProtocol.self, factory: SharedViewModel.init)
register(factory: VCFactory.init)
self.register(storyboardType: AViewController.self, tag: "A")
    .resolvingProperties { container, vc in
        vc.viewModel = try container.resolve() as SharedViewModelProtocol
        vc.factory = try container.resolve() as VCFactory
    }
Narayane commented 5 years ago

Would it be technically possible to have a "real" shared scope for a given object instance between different dependencies resolution graphs?

ilyapuchka commented 5 years ago

that's what singleton scope is for 🤷‍♂️ you maybe can use weakSingleton if you need to recreate an instance at some point.

Narayane commented 5 years ago

Technically not. For me, singleton scope lasts while dip container exists whereas shared scope "as I hear" lasts while at least one dependencies graph references it.

ilyapuchka commented 5 years ago

what you described is a weekSingleton =)

Narayane commented 5 years ago

@ilyapuchka It seems indeed. In my mind, this scope was only useful for breaking singletons cyclic injections... but I have one question again :)

I observe this:

So, how can I force your you maybe can use weakSingleton if you need to recreate an instance at some point at each AViewController dependencies graph new resolution?

self.register(.singleton) { try Repository(graphQL: self.resolve(), fileStorage: self.resolve()) as RepositoryProtocol }
self.register(.weakSingleton) { try SharedViewModel(repository: self.resolve()) as SharedViewModelProtocol }
self.register(storyboardType: AViewController.self, tag: "A")
    .resolvingProperties { container, vc in
        vc.viewModel = try container.resolve() as SharedViewModelProtocol
    }
self.register(storyboardType: BViewController.self, tag: "B")
    .resolvingProperties { container, vc in
        vc.viewModel = try container.resolve() as SharedViewModelProtocol
    }
Narayane commented 5 years ago

@ilyapuchka?

ilyapuchka commented 5 years ago

@Narayane you should probably register two weakSingletons of your view model with different tags, maybe reusing tags you use for view controllers. Then when you resolve the second stack when you need to resolve this new view model you need to use a different tag and then the new instance will be created and associated with this tag.