Keypaths and subclassing

I’m toying with a type-safe form library for iOS. The key idea is having a view model that the form is generic over:

struct ViewModel {
    var addCustomNote: Bool
    var customNote: String?
}

// Simplified example, ignoring sections
let form = Form<ViewModel>()
form.addRow(FormSwitchCell(keyPath: \.addCustomNote, title: "Add Custom Note"))
form.addRow(FormTextFieldCell(keyPath: \.customNote))

Now, the cells have various other properties that I would like to “bind” to the model, for example to hide cells that are not appropriate at the moment. For that I have an API like this:

// Cell will only appear when the the value at \.addCustomNote is true
form.addRow(FormTextFieldCell(keyPath: \.customNote)) { cell in
    cell.bind(\.isVisible, to: \.addCustomNote)
}

Now to the point: The cells have some hierarchy, I have a base FormCell<Model> and other cell types like FormTextFieldCell<Model>: FormCell<Model> etc. The bind method is defined on FormCell:

public func bind<T>(
    _ viewPath: WritableKeyPath<FormCell, T>,
    to modelPath: KeyPath<Model, T>) {
    // implementation
}

But that makes only possible to bind the properties from the base FormCell, whereas I would also like to bind properties introduced in FormCell subclasses. In other words, I want something like this:

public func bind<T>(
    _ viewPath: WritableKeyPath<Self, T>, // Self here instead of FormCell
    to modelPath: KeyPath<Model, T>) {
    // implementation
}

…but that’s not possible. I know I can do that as a free function (bind<Cell: FormCell>(…)), but then the API kind of sucks, as I don’t know where to stick the free function and how the user is going to find it. How would you do this?

I had this issue in my app too. I solved it by having a private protocol where Self is constrained to the base class. I defined as a requirement and then implemented the method on the protocol, with its key path parameter having a base type of Self. Then, I declared conformance to this private protocol on the base type via an empty extension. If you need to be able to override your bind method though, that could complicate things.

Let me know if that wasn’t clear.

2 Likes

I read the original post a few times by now and I'm not sure I follow. Is the issue the missing covariance on a generic type like WritableKeyPath or the lack of Self because SE-0068 was never implemented for more then two years?

In case of latter you can try moving that implementation into a protocol extension instead because there you'll get access to Self again. Here is an unrelated topic where I used that trick to force Self on subclasses: ios - Loading a XIB file to a UIView Swift - Stack Overflow

Thank you, that worked! Both code completion and I are now slightly baffled by all the type trickery, but that will pass.

Simplified sample (that I should have written earlier, sorry):

class Animal {
    var name: String
    func someKeyPathMethod<T>(path: KeyPath<Animal, T>) {}
}

class Dog: Animal {
    var numberOfLegs: Int
}

Now, can I write someKeyPathMethod in a way that would also allow me to refer to \.numberOfLegs when calling it on a Dog instance?

SE-0068 feels like it’s close, but is it really it? Could I replace KeyPath<Animal, T> with KeyPath<Self, T> after SE-0068 is implemented? It doesn’t sound like it from the introduction:

Within a class scope, Self means "the dynamic class of self". This proposal extends that courtesy to value types and to the bodies of class members by renaming dynamicType to Self. This establishes a universal and consistent way to refer to the dynamic type of the current receiver.

I can already access Self in this context, but the compiler complains:

'Self' is only available in a protocol or as the result of a method in a class; did you mean 'Animal'?

1 Like

Here is the trick I used on stackoverflow applied to your example which allows what you want if the implementation of the method can remain fully generic.

protocol KeyPathHelper : AnyObject {}

// with this constraint you get even access to most of the `Animal` members
extension KeyPathHelper where Self : Animal {
  func someKeyPathMethod<T>(path: KeyPath<Self, T>) {
    // generic implementation
  }
}

class Animal : KeyPathHelper {
  var name: String = "Swifty"
}

class Dog : Animal {
  var numberOfLegs: Int = 4
}

let dog = Dog()
dog.someKeyPathMethod(path: \.numberOfLegs) // completely valid
dog.someKeyPathMethod(path: \.name)
(dog as Animal).someKeyPathMethod(path: \.name)

So my answer is, yes SE-0068 will do eventually the trick here.


It will be the same Self on classes as on protocols, so yes after the proposal ever makes into the language you can theoretically move the method from the protocol extension directly into the base class and use Self there. ;)

2 Likes

Awesome, glad it worked for you! It is a little messy, but gets the job done.

I've got to say, this solution was really great. Thanks for posting. I've been fighting this one myself.