1

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)
    }
}
Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
Jorge Revuelta
  • 822
  • 1
  • 8
  • 16

2 Answers2

2

It is difficult to recommend proper solution without knowing details of your application, but here are some suggestions:

container.register(ClassToInjectInjector.self) { _ in 
    self.loadClassToInjectFor(indexPath) 
}

In general, all register-ations should be done outside of your objects. Common setup ishaving one global Container, which contains all the registrations - you should look at them as instructions to build application objects without any implicit context. If your dependency needs to be created in the UITableViewController, you can pass it to resolve method as an argument:

container.register(ClassToInjectPresentable.self) { resolver, property in
    let tabBar = SomeTabBarViewController()
    tabBar.property = property
    return tabBar
}

// in UItableVIewController
container.resolve(ClassToInjectPresentable.self, 
                  argument: self.loadClassToInjectFor(indexPath))

Also this is usually a bad idea:

.initCompleted { r, p in
    ...
    self.navigationController.pushViewController(tabBar, animated: true)
 }

You should not mix application logic with DI - use Swinject purely for constructing your dependencies.

So your UITableViewController might look something like this:

func didSelectRowAtIndexPath {
    let classToInject = self.loadClassToInjectFor(indexPath)
    let tabBar = container.resolve(
        SomeTabBarViewController.self, argument: loadClassToInjectFor(indexPath)
    )
    navigationController.push(tabBar, animated: true)
}

As for your TabBar and its view controllers: how do the UIViewControllers get into TabBar? Is it possible to do something like this?

class SomeTabBarViewController {
    init(viewControllers: [UIViewController]) {
        ...
    }   
}

container.register(SomeTabBarViewController.self) { r, property
    SomeTabBarViewController(viewControllers:[
        r.resolve(MyViewController.self, argument: property),
        r.resolve(MyViewController2.self, argument: property)
    ])
}
Jakub Vano
  • 3,833
  • 15
  • 29
1

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.

Jorge Revuelta
  • 822
  • 1
  • 8
  • 16