Declaring a property with closure argument typed to Self

Hi All.

Let me set up the situation:

We're creating an abstract ViewController superclass (ABSViewController) with some refactored callback functionality. So it contains a property for the callback function like (VCArg)->Void (sends the callback with myself as the argument). But, this class is never actually used since concrete subclasses are created in my application (MyVieController). Is there a way in ABSViewController to declare the closure argument as "Self" so that the client will have the correct class in the callback on the way out?

Example code:

class ABSViewController<R,I> : UICollectionViewController {
    var successCallbackBlock:((Self)->Void)?  // This does not work, but I want it to!
}

class MyViewController : ABSViewController {
    func someMethodThatIsNotInABSViewController() {}
}

class MyClient {
    let vc = MyCollectionViewController()
    vc.successCallbackBlock = { (bVC) in
        bVC.someMethodThatIsNotInABSViewController()
    }
}

Not really; this would violate the Liskov Substitution Principle. Consider the following code:

class Base {
  var successCallbackBlock: ((Self) -> Void)?
}

class Sub1: Base {
  func doSub1Things()
}
class Sub2: Base {}

let sub1 = Sub1()
let sub2 = Sub2()

sub1.successCallbackBlock = { $0.doSub1Things() }
let base1: Base = sub1
let base2: Base = sub2
base2.successCallbackBlock = base1.successCallbackBlock

sub2.successCallbackBlock(sub2) // uh oh!

I see... Thanks!

Actually you can do this, you just need to make ABSViewController a protocol instead of a base class.

Ideally, you'd want to be able to specify it as protocol ABSViewController: UICollectionViewController but that isn't quite supported yet (although it is coming), so you have to do this:

protocol ABSCallbackProtocol: AnyObject {
    var successCallbackBlock: ((Self) -> Void)? { get set }
}

typealias ABSViewController = UICollectionViewController & ABSCallbackProtocol

// If you can, make classes final.
final class MyViewController: ABSViewController {
    var successCallbackBlock: ((MyViewController) -> Void)?
    func someMethodThatIsNotInABSViewController() {}
}

final class MyClient {
    let vc = MyViewController()
    vs.successsCallbackBlock = { vc in
        vc.someMethodThatIsNotInABSViewController()
    }
}

This should do exactly what you want.

1 Like

Good point, Self in a protocol is different from Self in a class.

This may work, but I also see nothing that prevents you from doing the substitution thing in Jordan's example, so isn't this unsafe?

The compiler will keep you from using the protocol as a type when it has Self in its requirements. But you said you never actually need to use the base class as a type, so that's not a problem for your use case.

@jrose In addition to that, only a final class is allowed to implement this protocol, so there can't be any subclassing issues in the first place.

1 Like

When I try to do this, I get the error:

Protocol 'CallbackProtocol' requirement 'successCallbackBlock' cannot be satisfied by a non-final class ('ABSCollectionViewController<R, I>') because it uses 'Self' in a non-parameter, non-result type position

So it looks like I can't create an abstract superclass with factored out functionality, thus killing the original idea.

If you declare all the variables you need in a protocol, then you can write all default behavior you need in a protocol extension. No abstract base class needed. Do you have any examples of the behavior you're trying to inherit?

1 Like

I want to implement the UICollectionViewDataSources methods

override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell

and I want them to still be overrideable in the subclass if necessary.

a) I don't know if implementing these in an protocol-satisfying extension will properly register them with the ObjC runtime so that they will get called.
b) I don't believe you can override protocol extension methods in a subclass, can you?
c) Besides which, I don't think I want to then have implementation of these methods on all UICollectionViewControllers.

That is trickier. You can implement these in an extension, but the problem is that they're already implemented by UICollectionViewController and Swift doesn't currently let you override in an extension. The best you can do is to implement this extension:

extension ABSCallbackProtocol where Self: UICollectionViewController {
    func numberOfItems(from view: UICollectionView, section: Int) -> Int {
        // Insert default implementation here.
    }
    func cell(from view: UICollectionView, at index: IndexPath) -> UICollectionViewCell {
        // Insert default implementation here.
    }
}

Then manually call those functions from your overrides.

There's still a way to do this though, make your abstract base class with the overrides then have ABSViewController defined as:

typealias ABSViewController = MyAbstractViewController & ABSCallbackProtocol

That's assuming that you don't need the success callback in order to implement the overrides. If you do, then I'm out of ideas.

At this point, it's a lot of machinery to avoid doing a single cast of an argument in a block. I think I'll just stick with the simple way. Thanks for the insights - I'm sure it will all come in handy later.

As far as I understand doesn't Swift have support for abstract classes (thx Thor for that).

Right. Neither does Objective-C, but it's still a useful concept. It doesn't really require strict compiler support, and you can do it by convention. An abstract superclass is just a class that we all agree isn't useful to instantiate.

Yes, it's easy to workaround by just having any methods that are required to be overridden trap if the default is called. But truthfully, the only time I had to do something like that was to work around the lack of generalized existentials.

I may show my stupidity but what are these?

This is currently not allowed:

protocol MyProtocol {
  associatedtype MyType
}

func takesMyProtocol(_ p: MyProtocol) { } // Protocol 'MyProtocol' can only be used as a generic constraint because it has Self or associated type requirements

More about them

Ok thx for trying - didn't get a single word from the linked document for existentials.

If you search the forums for that phrase, you'll probably get a million hits about that limitation. It's a very common request to lift it.