1
votes

I'm trying to abstract views that are configured from a view model. I've been using associated types so far:

public protocol ViewModelProtocol: Equatable {}

public protocol ModeledView: class {
    /// The type of the view model
    associatedtype ViewModel: ViewModelProtocol

    var viewModel: ViewModel? { get }

    /// Sets the view model. A nil value describes a default state.
    func set(newViewModel: ViewModel?)
}

Which I can use like:

struct MyViewModel: ViewModelProtocol {
    let foo: String

    static public func == (lhs: MyViewModel, rhs: MyViewModel) -> Bool {
        return lhs.foo == rhs.foo
    }
}

class MyView: UIView, ModeledView {
    typealias ViewModel = MyViewModel

    private(set) var viewModel: MyViewModel?

    public func set(newViewModel: MyViewModel?) {
        print(newViewModel?.foo)
    }
}

However I'd like to specify a protocol for my view models, and not a concretized type. The reason is that one struct / class could comply to several such view model protocols. I don't want to either convert this object to another type just to pass it to the view, or have the view have an associated type with more requirements than it needs. So I think I'd like to do something like:

protocol MyViewModelProtocol: ViewModelProtocol {
    var foo: String { get }
}

class MyView: UIView, ModeledView {
    typealias ViewModel = MyViewModelProtocol

    private(set) var viewModel: MyViewModelProtocol?

    public func set(newViewModel: MyViewModelProtocol?) {
        print(newViewModel?.foo)
    }
}

struct DataModel: MyViewModelProtocol {
    let foo: String

    let bar: String

    static public func == (lhs: MyViewModel, rhs: MyViewModel) -> Bool {
        return lhs.foo == rhs.foo && lhs.bar == rhs.bar
    }
}

let dataModel = DataModel(foo: "foo", bar: "bar")
let view = MyView()
view.set(newViewModel: dataModel)

This doesn't work. The compiler says that MyView doesn't conform to the ModeledView protocol, and hints that

Possibly intended match 'MyView.ViewModel' (aka 'MyViewModelProtocol') does not conform to 'ViewModelProtocol'

I don't really get what's bothering the compiler as MyViewModelProtocol is defined as extending ViewModelProtocol

1
There are a lot of intersecting problems here, but this entire approach is extremely difficult to build in Swift today. What you want to do is to start with concrete type (things actually in your program), and extract a solution that solves any particular abstraction problem you are facing. That generally works quite well, and tends to solve most problems in shipping apps. Trying to build this "as abstractly as possible" without a clear problem you're solving will tend to get you in a mess. - Rob Napier
Do you have an actual program with code duplication that you're trying to solve? If you post concrete code, in my experience, we can solve it in most cases. The place it goes sideways is when you start with "I want it to be abstract" without a concrete problem to solve. For the much longer version, see robnapier.net/start-with-a-protocol - Rob Napier
@RobNapier Hey Rob, as a curious and novice bystander, I'm curious what things are missing that leads you to conclude "this entire approach is extremely difficult to build in Swift today". Care to elaborate? - Alexander
@Alexander Because ModeledView is a PAT (protocol with associated type), but it's clearly something that you'd want to put in a variable or an array. PATs have no existential type, so you can't do that. This is what eventually leads people down the road to type erasers, and it rapidly becomes a mess. The point of a PAT is to add methods to other types or to allow a type to be passed to a generic function, not to be a "generic protocol." Swift is evolving to allow that (forums.swift.org/t/improving-the-ui-of-generics/22814), but I wouldn't expect it for several releases. - Rob Napier
I'm poking around currently at whether this has a more proper answer, but even if the immediate problem is fixed, several more are very likely to pop up immediately. - Rob Napier

1 Answers

2
votes

MyViewModelProtocol is defined as extending ViewModelProtocol

Correct. MyViewModelProtocol extends ViewModelProtocol. It doesn't conform to ViewModelProtocol. This is a classic case of "protocols do not conform to themselves." Your associated type requires that ViewModel be a concrete type that conforms to ViewModelProtocol, and MyViewModelProtocol is not a concrete type and it does not conform to anything (protocols do not conform to protocols).

The way to attack this problem is to start with the calling code, and then construct protocols that support what you want the calling code to look like. In the code you've given, the correct solution is to get rid of ModeledView entirely. It isn't doing anything; nothing relies on it, and it doesn't provide any extensions. I assume in your "real" code, something does rely on it. That's the key piece of code to focus on. The best way to achieve that is to write a few concrete examples of the kinds of conforming types you'd like to exist. (If you have trouble writing several concrete implementations, then reconsider whether there's anything to abstract.)


Just to elaborate a bit more why this won't work in your specific case (it won't work in more general cases either, but your case layers additional restrictions that are important). You've required that types that conform to ViewModelProtocol also conform to Equatable. That means they must implement:

static public func == (lhs: Self, rhs: Self) -> Bool {

This does not mean that some type conforming to ViewModelProtocol is Equatable to some other type conforming to ViewModelProtocol. It means Self, the specific, concrete type implementing the protocol. ViewModelProtocol is not itself Equatable. If it were, which code above would you expect dataModel == myViewModel to call? Would it check bar or not? Remember, == is just a function. Swift doesn't know that you're asking "are these equal." It can't inject things like "if they're different concrete types they're not equal." That's something the function itself has to do. And so Swift needs to know which function to call.

Making this possible in Swift will be a major addition to the language. It's been discussed quite a lot (see the link I posted in the earlier comments), but it's at least several releases away.

If you remove the Equatable requirement, it still won't work today, but the language change required is much smaller (and may come sooner).