Saturday, August 18, 2018

A modest proposal for optional methods in Swift protocols

Swift contains protocols in which you can declare methods, which something claiming to adhere to the protocol must implement. One thing Swift protocols can’t do are optional methods... sort of. In Objective-C we’d specify an optional method in a protocol like so

@protocol MyProtcol<NSObject>
-(void)someRequiredMethod:(id)param;
@optional
-(void)someOptionalMethod:(id)param;
@end

In Swift you can put the optional keyword next to a method if the protocol is annotated with @objc.

@objc protocol MyProtocol {
func someRequiredMethod()
optional func someOptionalMethod()
}

This solves the problem of optional methods in a protocol, but makes us forever dependent on Objective-C for this feature. Additionally you can only use this with classes, and not structs. This should be possible in Swift alone without relying on Objective-C features. There are some other options, like you could split the protocol up into 2 protocols like so

protocol MyProtocol {
func someRequiredMethod()
}

protocol MyOptionalProtocol {
func someOptionalMethod()
}

And finally the more elegant solution is to provide a default implementation for the optional methods in a protocol extension. This feels like the native Swift way to do it.

protocol MyProtocol {
func someRequiredMethod()
func someOptionalMethod()
}

extension MyProtocol {
func someOptionalMethod() {}
}

This works great, but is poor at communicating intent. In this particular case it is easy to spot the optional method, but that will not be the case with any non trivial project. I want to signal to future developers that these method(s) are optional at the place I declare a protocol, without forcing you to read each methods documentation, or requiring you to read 2 chunks of code, connect the dots, and then realize that the method is intended to be optional. You should be able to glance at a protocol, and easily understand what is optional, and what is not.

The Solution

The solution it seems should be rather simple as it mostly just enforces what the swift compiler already does. At least as of Xcode 10 if you
(1) declare a protocol
(2) provide a default implementation for the optional methods in a protocol extension and
(3) declare something as adhering to the protocol, and you haven’t implemented any of the methods
Then Xcode will only stub in your “required” methods in a fix it, after you declare a type that adheres to the protocol.

So why don’t we just enforce this practice in the compiler? It would work like this. You would annotate a func in a protocol with the optional keyword like so.

protocol MyProtocol {
func someRequiredMethod()
optional func someOptionalMethod()
}

If you haven’t provided a default implementation in a protocol extension, you will get an error/fix-it like this

"Error. Function annotated with optional in a protocol declaration must have a corresponding default implementation. Add stub(s) for the default implementation?"

For this case we’d just implement an empty method body since it doesn’t return anything

extension MyProtocol {
func someOptionalMethod() {}
}

This way we both declare intention at the place the protocol is declared, and avoid optionals in dealing with the protocol. Under this new system everything pretty much works as it already does, the only difference is the compiler now lets us annotate our intention and enforces a practice swift devs already use for swift protocols.

Thanks to Rob Napier and Mustapha for discussing this issue with me, which lead to this solution.

Update: Accidentally referred to Rob Napier as Rob Rix, this has been fixed in the article, sorry for the confusion & sorry Rob for the mixup.

 
...