Search code examples
iosswiftdependency-injectionswinject

Swinject inject self's property into new UIViewController


Let's pretend we have an UITableViewController that on didSelectRowAtSection loads an instance of a class named i.e.: ClassToInject and it wants to inject it through a property injection because our ViewControllerToBePushed has a property of ClassToInject, that subsequently (because it's an UITabBarViewController) on the didSet callback it searches for all its viewControllers property that conforms to ClassToInjectPresentable simple as:

protocol ClassToInjectPresentable { 
  var property: ClassToInject { get set } 
}

Until now, i would just do something like this:

func didSelectRowAtIndexPath {
     let classToInject = self.loadClassToInjectFor(indexPath)
     let tabBarViewController = SomeTabBarViewController()
     tabBarViewController.property = classToInject
     self.navigationController.push(tabBarViewController, animated: true)
}

And in SomeTabBarViewController ...

class SomeTabBarViewController: ClassToInjectPresentable {
  var property: ClassToInject? {
  didSet(newValue) {
      self.viewControllers.filter{ $0 is ClassToInjectPresentable }.map{ $0 as! ClassToInjectPresentable }.forEach{ $0.property = newValue }
  }
 }

And everything should be get loaded nice and easy (but it's not). I've read about Swinject and this might be solved with it. I have seen lots of examples registering things like:

container.register(Animal.self) { _ in Cat(name: "Mimi") }

But I don't know if I can register some property that is loaded in self:

container.register(ClassToInjectInjector.self) { _ in 
self.loadClassToInjectFor(indexPath) }
// And then
container.register(ClassToInjectPresentable.self) { _ in 
SomeTabBarViewController() }
    .initCompleted { r, p in
        let tabBar = p as! SomeTabBarViewController
        tabBar.property = r.resolve(ClassToInjectInjector.self)
        // And lastly?
        self.navigationController.pushViewController(tabBar, animated: true)
    }
}

Solution

  • Finally I got the final answer by following the suggestions proposed.

    public class Containers {
        fileprivate init() { }
    }
    
    extension Containers {
        static let activityPresentableContainer: Container = {
            let container = Container()
            container.register(ActivityTabBarController.self) { (r: Resolver, arg1: Activity) in
                return ActivityTabBarController(activity: arg1)
            }
            container.register(ActivityPresentable.self) {
               (r: Resolver, arg1: ActivityPresentableTabs, arg2: Activity) in
               switch arg1 {
               case .summary:
                   return ActivitySummaryViewController(activity: arg2)
               case .detail:
                   return ActivityDetailPageViewController(activity: arg2)
               case .map:
                   return ActivityMapViewController(activity: arg2)
               case .charts:
                   return ActivityChartsViewController(activity: arg2)
               case .strava:
                  return ActivityStravaViewController(activity: arg2)
               }
            }.inObjectScope(.transient)
           return container
        }()
    

    With this approach, the named ActivityTabBarController gets instantiated always by the activityPresentableContainer using the following statement:

    let controller = Containers.activityPresentableContainer.resolve(
        ActivityTabBarController.self, argument: activity
    )!
    

    And then, each of the tabs inside the TabBarController gets instantiated using the required argument Activity and the type of tab itself using a .transient context. It resolves like this:

    let activitySummary = Containers.activityPresentableContainer.resolve(
       ActivityPresentable.self, arguments: ActivityPresentableTabs.summary, activity!
    ) as! UIViewController
    

    This way I can generalize the tabs of the tab bar depending just on the information that they're using. If one of the tabs change in any moment, I can just change the registration, following the ActivityPresentable protocol.