In a sub-class, is it possible to override a property of the super-class and constrain to a sub-type of the original property?

I'm struggling with the vocabulary to express this, but I'm extending some UIKit view classes for which I also need to extend the capabilities of the counterpart delegate protocol. Attempting to override the delegate property and specifying my own delegate as the type (which is a sub-type of the overridden property type) doesn't seem to be an option.

However, it must be possible somehow as Apple is using the mechanism in its own UIKit implementations. e.g. the delegate property on UITableView overrides(?) the delegate property on UIScrollView and constrains it to the UITableViewDelegate protocol which is a sub-type of UIScrollViewDelegate.

In pseudo-code:

protocol UIScrollViewDelegate { ... }
class UIScrollView {
    ...
    weak var delegate: UIScrollViewDelegate?
}

protocol UITableViewDelegate: UIScrollViewDelegate { ... }
class UITableView: UIScrollView {
    ...
    weak var delegate: UITableViewDelegate?
}

I can't seem to get this to work in my own code, so is this a restriction of swift that is somehow overlooked when dealing with obj-c code, or is there another way to do this?

I can help with the vocab part: you want covariant properties.

I don’t know how UIKit makes it work, but from a type-theory perspective it is unsound.

After all, since UITableView is a subclass of UIScrollView, you can store a UITableView instance in a variable of type UIScrollView. And then you can set the delegate of that variable to a UIScrollViewDelegate which is *not* a UITableViewDelegate.

See the problem?

1 Like

Covariant properties, huh? Thanks for the vocab. Ah yes, that could be nasty.

I guess one solution in that situation would be something along the lines of the super.delegate returning a UIScrollViewDelegate and self.delegate only returning a UITableViewDelegate if indeed the delegate does conform – but I can see how that could become troublesome.

Thanks!

I don’t know how UIKit makes it work, but from a type-theory perspective it is unsound.

I think in this particular case it would actually be (almost*) sound because UITableViewDelegate has no required methods, only optional ones. So in a sense every UIScrollViewDelegate is a valid UITableViewDelegate.

* “almost” because dynamic checks like [delegate conformsToProtocol:@protocol(UITableViewDelegate)] might not work as expected.

I'm afraid you can't do this, because it violates the Liskov substitution principle and allows for unsafe code:

let table: UITableView = …
let scrollView: UIScrollView = table
scrollView.delegate = justAScrollViewDelegateNotATableViewDelegate
table.delegate?.tableView?(…) // ???

As Ole points out, the fact that UITableViewDelegate only has non-optional requirements makes this nearly work in Objective-C, but it's still a generally unsafe pattern, and I wish UIKit didn't use it.

EDIT: Oops, I forgot to actually answer the question. We think it's better for Swift to model what Objective-C says it's doing than to enforce this strictly, but native Swift properties are still not allowed to do this, because Swift uses compile-time knowledge about types more than Objective-C does.

2 Likes

Nothing seems to prevent Swift from being able to follow the Liskov principle in this case. If a refinement protocol only adds optional requirements or only requirements with default implementations, that code can be made safe together with the UIScrollView - UITableView quite reasonable subtyping behavior in having a refined delegate. What is the reason for this being unsafe in the general case?

protocol A { func foo() }

protocol B: A { func soo() }

extension B { func soo() {} } // or @optional

class Foo: A { func foo() {} }

let a: A = Foo()

let b: B = a as! B // SIGABRT

So, in other words, this is a very deep lying and complicated problem to solve?

In Objective-C conforming to a protocol gives you two things: you can send messages in the protocol to objects with that static (compile-time) implementing type without the compiler shouting at you, and you can check -conformsToProtocol:. As Ole pointed out, the violation I showed above breaks the latter use case, but not the former.

In Swift, conforming to a protocol means that there's a table of operations that you can invoke for the concrete type, which is either passed separately (generic functions), stored in the type (generic types), or stored next to the value (protocol values, the way most delegates are defined). That means that if you want to have a stronger type in a subclass, you need new storage, because the superclass doesn't have space for the new table. Additionally, if you actually provide that new storage, you'd have to trap at runtime if someone tried to provide a type that didn't conform to the protocol through the base class's interface (the way your example showed). That's implementable, but that does count as violating Liskov substitution, because something that's legal for normal UIScrollViews would cause a trap for UITableViews.

Now, people do make operations trap at runtime all the time, even when their superclass pretends to support it. But since there are other solutions to this problem that we think are better, it's not a feature we're interested in supporting.


class MyTableView1: MyScrollView {
  weak var tableDelegate: MyTableDelegate? {
    didSet { self.delegate = self.tableDelegate }
  }
}

class MyTableView2: MyScrollView {
  var tableDelegate: MyTableDelegate? {
    get { return super.delegate as! MyTableDelegate? }
    set { super.delegate = newValue }
  }

  override var delegate: MyScrollDelegate? {
    get { return super.delegate }
    set {
      precondition(newValue is MyTableDelegate?)
      super.delegate = newValue
    }
  }
}
1 Like

Interesting. So, if I understand correctly, the reason UIKit is able to do this is because in obj-c – even though it's potentially unsafe – the compiler doesn't object. Swift, being the safety conscious language that it is, does.

Therefore, we see some inconsistency between what is possible in a bridged obj-c type hierarchy and what is possible in a pure swift type hierarchy.

Makes sense.

I'm looking forward to seeing more of those pure swift frameworks.... :)

1 Like

My apologies, in my example B is supposed to refine A (edited), but it makes no difference anyway.

Now I see, thank you. This becomes much more obvious if tried against inheritance:

class A {
    var view: UIView
}

class B: A {
    override var view: UIButton
}

let b = B()
let a: A = b
a.view = UIView()
b.view.(...) ??

It seems logical at first, but is actually very unsafe and indeed does violate Liskov substitution.

Moreover, it appears the optional attribute produces optional methods in the sense of (A -> B)? . I wonder if this is done to compensate for breaking the rules by adding safety.

1 Like