3
votes

I have a Swift 3 project where I'm declaring a protocol with an associated type like this:

protocol ViewModelContainer {
    associatedtype ViewModelType
    var viewModel: ViewModelType! { get set }
}

And I want to check if two objects implement ViewModelContainer and it's associated type ViewModelType to make the assignment in a 'generic' way.

Ideally I'd like to do something like this:

if let container = container as? ViewModelContainer, let model = model as? container.ViewModelType {
    container.viewModel = model
}

But I can't cast container to ViewModelContainer:

Protocol 'ViewModelContainer' can only be used as a generic constraint because it has Self or associated type requirements


My current workaround is to fall back to specific classes and their associated types directly, but it leaves my code very verbose and error prone:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if let vc = segue.destination as? MediaPlaySelectionViewController, let vm = sender as? MediaPlaySelectionViewModel {
        vc.viewModel = vm
    }
    if let vc = segue.destination as? SearchResultsViewController, let vm = sender as? SearchResultsViewModel {
        vc.viewModel = vm
    }
    if let vc = segue.destination as? ReviewDetailsViewController, let vm = sender as? ReviewDetailsViewModel {
        vc.viewModel = vm
    }
    if let vc = segue.destination as? ReviewComposerViewController, let vm = sender as? ReviewComposerViewModel {
        vc.viewModel = vm
    }
}

I tried using generic UIViewControllers, but got stuck because Objective-C doesn't recognize generic Swift classes and therefore can't be used in Storyboard.

2
This is really stupid... but if you make an empty protocol, and then make all types conforming to ViewModelContainer conform to that protocol, then you check for conformance to that protocol without bumping up against this issue.BallpointBen
I guess the real question is why isn't container statically typed as a type that conforms to ViewModelContainer? You may well be looking for a type eraser.Hamish
Why not make the ViewModelType as a protocol?dichen
@Hamish UIKit hates generics and I'm being forced to use Any at some point to workaround itredent84
@dichen I already have a ViewModel protocol. But it doesn't help here because each container must contain its specific view model class.redent84

2 Answers

0
votes

Here is the idea of changing the associatedtype ViewModelType to a protocol.

protocol ViewModelProtocol {
}

protocol ViewModelContainer {

    var viewModel: ViewModelProtocol? { get set }
}

class MediaPlaySelectionViewModel: ViewModelProtocol {

    var title: String?
    func play() {
        print("playing")
    }
}

class SearchResultsViewModel: ViewModelProtocol {

    var results: [String]?
}

class MediaPlaySelectionViewController: UIViewController, ViewModelContainer {

    var viewModel: ViewModelProtocol?

    // So the view itself know which kind of vm it wants.
    var myViewModel: MediaPlaySelectionViewModel? {
        return viewModel as? MediaPlaySelectionViewModel
    }

    override func viewWillAppear(_ animated: Bool) {
        print(myViewModel?.title ?? "Undefined")
    }
}

class SearchResultsViewController: UIViewController, ViewModelContainer {

    var viewModel: ViewModelProtocol?

    // So the view itself know which kind of vm it wants.
    var myViewModel: SearchResultsViewModel? {
        return viewModel as? SearchResultsViewModel
    }

    override func viewWillAppear(_ animated: Bool) {
        print(myViewModel?.results?.joined(separator: ", ") ?? "No Result")
    }
}

class MenuViewController: UITableViewController {

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

        // NOTE: Swift doesn't allow me to use 'let' here
        if var container = segue.destination as? ViewModelContainer, let cell = sender as? UITableViewCell, let vm = viewModel(for: cell) {
            container.viewModel = vm
        }
    }

    // NOTE: One difficulty here, how could you decide which ViewModel to prepare?  I guess you need a Factory.
    func viewModel(for cell: UITableViewCell) -> ViewModelProtocol! {

        if let index = tableView.indexPath(for: cell) {

            if index.item == 0 {

                let vm = MediaPlaySelectionViewModel()
                vm.title = "My Video"

                return vm
            }
            else if index.item == 1 {

                let vm = SearchResultsViewModel()
                vm.results = ["Apple", "Banana"]

                return vm
            }
        }

        return nil
    }
}
0
votes

It was trickier than I expected (so I deleted my previous post to avoid confusion) but I believe this should work for you:

 protocol ViewModelContainerVC 
 {
   mutating func setModel(_ :Any)
 }

 protocol ViewModelContainer:class,ViewModelContainerVC 
 {
     associatedtype ViewModelType
     var viewModel: ViewModelType! { get set }
 }

 extension ViewModelContainer
 {
    mutating func setModel(_ model:Any)
    { if model is ViewModelType { viewModel = model as! ViewModelType } }
 }

You can then use the ViewModelContainerVC for type casting and assignment:

 if let container = container as? ViewModelContainerVC 
 { 
   container.setModel(model)
 }

[EDIT] for future reference, here's the same thing with a Bool return for type compatibility:

 protocol ViewModelContainerVC 
 {
   @discardableResult mutating func setModel(_ :Any) -> Bool
 }

 extension ViewModelContainer
 {
    @discardableResult mutating func setModel(_ model:Any) -> Bool
    { 
       if let validModel = model as? ViewModelType 
       { viewModel = validModel; return true }
       return false 
    }
 }

Which will allow a combined condition :

 if var container = container as? ViewModelContainerVC,
    container.setModel(model) 
 { ... }