AliSoftware/Dip

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

Narayane opened this issue · 11 comments

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

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.

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.

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.

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
    }

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

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

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.

what you described is a weekSingleton =)

@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:

  • first time I launch AViewController, Dip logs Resolved type SharedViewModelProtocol with SharedViewModel
  • then I push BViewController from AViewController, Dip logs Reusing previously resolved instance SharedViewModel that it is perfect!
  • then I go to another part of my app (DViewController then RViewController then ...) with no relation with AViewController or BViewController dependencies graphs
  • then I relaunch AViewController and Dip logs Reusing previously resolved instance SharedViewModel that I don't want, I want a fresh instance of my SharedViewModel
  • then, when I will repush BViewController from AViewController (if I will), it uses the fresh SharedViewModel instance

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 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.