goldengil
goldengil

Reputation: 1037

How to inject the right viewModel to a single viewController

I'm using Swinject for my DI solution, and extend it with the SwinjectStoryboard extension.

I'm struggling with dynamic injecting the right viewModel to a specific viewContoller. The specific scenario is as follow:

MyViewController has a property called var viewModel: ViewModeling.

There are 2 different view models that are conforming to the ViewModeling protocol, lets call them: firstViewModel and secondViewModel. My storyboard contain just one controller and its the MyViewController.

The Issue

Inject the right viewModel as a dependancy of MyViewController dynamically (so only while running I'll know if to inject the first or the second)

I am able to do it on the service level (one protocol that 2 services are conforming to, and 2 different viewModel that each consume a different service can resolve the required one using a specific name)

I'm struggling with doing this on the viewController level, trying to inject the same view controller a specific viewModel (both conforms as mentioned to the same protocol).

Currently my hunch is that SwinjectStoryboard doesn't let me instantiate a view controller using its storyboard id (like I would normally do), and in addition define few different names that will be resolved on runtime.

Am I missing something?

Upvotes: 2

Views: 3494

Answers (2)

Denis Kutlubaev
Denis Kutlubaev

Reputation: 16114

My patterns, how to inject ViewModel to ViewController:

// ------------------------------------------------------------------------------------------
// Option 1, when you use Storyboards
// ------------------------------------------------------------------------------------------

import Foundation

class MyViewController {

    var viewModel: MyViewModel!

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var titleLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        bindViewModel()
    }

    func bindViewModel() {

    }

    // Then, you can inject ViewModel in prepare function
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        super.prepare(for: segue, sender: sender)

        guard let myViewController = segue.destination as? MyViewController else { return }
        myViewController.viewModel = MyViewModel(myDependency: myDependency)
    }
}

// ------------------------------------------------------------------------------------------
// Option 2, when you use xibs
// ------------------------------------------------------------------------------------------

import Foundation

class MyViewController {

    let viewModel: MyViewModel

    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var titleLabel: UILabel!

    init(viewModel: MyViewModel) {
        self.viewModel = viewModel

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        bindViewModel()
    }

    func bindViewModel() {

    }

    // Then, you can inject ViewModel in constructor:
    func showMyViewController() {
        let vm = MyViewModel(myDependency: myDependency)
        let vc = MyViewController(viewModel: vm)
        self.present(vc, animated: true, completion: nil)
    }
}

Upvotes: 0

Jakub Vano
Jakub Vano

Reputation: 3873

You haven't missed anything - the behaviour you are looking for is indeed currently not possible to achieve with SwinjectStoryboard.

You can have multiple storyboardInitCompleted with different names, but they correspond to the names entered in the storyboard parameter swinjectRegistrationName (see docs for more info) - in order to use this you would need to have multiple copies of the view controller in the storyboard.

From my point of view the ideal solution to this would be the registration arguments, i.e. you would have an enum ViewModelType {} which would be used in the storyboardInitCompleted to resolve the correct view model. Unfortunately, this feature is not yet finalised (see this PR).

In the current state I would probably try moving the selection of the view model from the injection logic to application logic - i.e. you could have some

protocol ViewModelProvider {
    var viewModel: ViewModeling { get }
}

which would be injected into the view controller and provide correct view model based on some application state.

Another way to circumvent the issue would be to ditch the SwinjectStoryboard registration, and use basic Swinject to instantiate the view controller:

container.register(MyViewController.self, name: "name") {
    let vc = SwinjectStoryboard.create(name: "MyStoryboard", bundle: nil).instantiateViewController(withIdentifier: "identifier")
    vc.viewModel = $0.resolve(ViewModeling.self)
    return viewModel
}

let vc = SwinjectStoryboard.defaultContainer.resolve(MyViewController.self, name: "name")

Upvotes: 2

Related Questions