2
votes

I am researching dependency injection and am currently in the process of updating my project to make use of it. However, I am having issues with associated type and protocol conforming.

I have created a quick demo project and I have created a few protocols and extensions so that viewControllers that conform to my protocol ViewModelBased must implement an associatedtype. Ideally I want this associatedtype to conform to a viewModel. Here is what I have so far

protocol ViewModel {
    associatedtype Services
    init (withServices services: Services)
}

protocol ViewModelBased: class {
    associatedtype ViewModelType
    var viewModel: ViewModelType { get set }
}

extension ViewModelBased where Self: UIViewController{
    static func instantiateController(with viewModel : ViewModelType) -> Self {

        // I have created UIStoryboard extension to allow for easy opening of view controllers
        // in storyboard
        let viewController : Self = UIStoryboard.mainStoryboard.instantiateViewController()
        viewController.viewModel = viewModel
        return viewController
    }
}

So all viewModels in my app conform to ViewModel which forces them to implement a service type. For example, my LoginModel looks like this

 struct LoginModel : ViewModel{

    // service type
    typealias Services = LoginService

    // init service
    var services : LoginService
    init(withServices services: LoginService) {
        self.services = services
    }

    /// calls login service - attempts login api
    func attemptLogin() {
        services.login()
    }
}

So here is an example of a viewController that implements this

class SecondController: UIViewController, ViewModelBased  {

    var viewModel: LoginModel!

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    @IBAction func loginTest() {
        viewModel.services.onLoginSuccess = { isVerified in

            print(isVerified)
        }

        viewModel.services.onLoginFailure = { errorCode in

            print(errorCode)
        }

        viewModel.attemptLogin()
    }
}

So putting this together, this allows the app to init a viewController and pass in a viewModel like this

   let loginModel = LoginModel(withServices: LoginService())
        let controller = SecondController.instantiateController(with: loginModel)
        self.navigationController?.pushViewController(controller, animated: true)

This all works very well, but the problem I am having is, the associated Type can be any type at the moment. Ideally I want this associatedType to conform to the ViewModel protocol. However when I try this

protocol ViewModelBased: class {
    associatedtype ViewModelType : ViewModel
    var viewModel: ViewModelType { get set }
}

My SecondController now throws an error and now forces me to init the LoginModel

var viewModel : LoginModel = LoginModel(withServices: LoginService())

But this is no longer making use of dependency injection as the viewController is now in charge of creating the viewModel instance and knows about the behavior of the viewModel class.

Is there a way I can fix this? Would be very grateful if someone could give me some information on what I am doing wrong.

1

1 Answers

1
votes

You could extend Optional to conditionally conform to ViewModel:

extension Optional: ViewModel where Wrapped: ViewModel {

    typealias Services = Wrapped.Services

    init(withServices services: Services) {
        self = Wrapped(withServices: services)
    }

}

Now your SecondController declares its model as var viewModel: LoginModel?, which it initializes to nil by default, while still ensuring that its ViewModelType is a ViewModel without actually needing to create an instance of it.