Optional protocols methods in Swift – right now – don’t exist unless the protocol is tagged with @objc. So far, this has been an acceptable solution for providing optional method functionality.

swift_objectiveC1

I will argue that this is a poor way to think about optional protocol swift methods. This particular solution is a relic of a dynamic language approach that we are familiar with as Objective-C developers. Swift code should take another approach.

Protocols serve a lot of different purposes, more so in Swift than in Objective-C. When a design calls for an optional method, it is typically for two reasons that are best exemplified by the delegate design pattern:

  1. Get something from the delegating object, like a value or a notification of an event. For example, this is how UIImagePickerController delivers its image and how UITextField notifies its delegate of changed text. These could be generalized as ‘callback’ protocol methods.
  2. Provide information back to the delegating object; either to dictate behavior or to provide dynamic values for some operation. UITableView‘s dataSource does this to provide row content and UITextFields uses this idea to filter keyboard input. These types of methods could be generalized as ‘provider’ protocol methods.

These are necessary operations, even in Swift. The implementation of optionality, however, should respect the features of the language.

An optional method is another way of saying “a method with default behavior.” A callback protocol method defaults to doing nothing, while a provider method has a default behavior or value. Required methods, on the other hand, do not have default behavior: consider what the ‘default’ should be for trying to archive an object that doesn’t conform to NSCoding? (Hint: it rhymes with rash.)

The Objective-C approach to implementing default behavior happens at the call site:

	
	// Providing a value; default to 0 if protocol method not implemented.
	int numericValue = 0;
	if ([self.delegate respondsToSelector:@selector(numericValueForObject:)] {
		numericValue = [self.delegate numericValueForObject:self];
	}
	
	// Controlling code flow; default to doLessWork if protocol method is not implemented.
	if ([self.delegate respondsToSelector:@selector(objectShouldDoMoreWork:)]
	&& [self.delegate objectShouldDoMoreWork:self]) {
		[self doMoreWork];
	} else {
		[self doLessWork];
	}

 

Optional protocol methods in Swift are a relatively recent addition to Objective-C. Prior to ObjC 2.0, optional protocol methods were implemented informally by adding categories to NSObject. Objects that wished to take advantage of delegate methods would override the NSObject implementation. The drawback here was that one couldn’t have required methods checked at compile-time; the method was always implemented by every object! (The workaround was having the default implementation fail in some way, requiring implementors to override the method to prevent the failure. That was a bummer: one could only find the error at runtime.)

In Swift, there is also no way to mark a protocol method as optional. One could probably argue that this is the necessary behavior of statically typed languages, but I won’t pretend to have considered every use case. Either way, it is the correct choice; there isn’t going to be a non-@objc optional annotation for Swift protocols (see note at end*). Applying @objc to protocols to get method optionality isn’t the solution, though – marking a protocol as @objc strips some of the Swift-power from a protocol.

Instead, we can borrow a page from old school Objective-C and improve upon it: Swift optional methods can be implemented by providing a default implementation in an extension. Any object conforming to the protocol gets the default implementation, and can optionally implement it to provide a different implementation. While similar to Objective-C informal protocols, this is better for two reasons: methods aren’t added to every single object, and we still get the compile-time failure behavior of required methods.

Now, the astute reader may be wondering about handling the case where the delegate is nil. In Objective-C, with protocols, we often did something that was actually rather unsafe: we molded methods to abuse the fact that NO, zero and nil were essentially the same. For example, methods like -objectShouldDoMoreWork: worked really well because the default is NO and sending this message to nil would return the same ‘value’ as NO. If the default were YES, we’d just change the method name to -objectShouldDoLessWork:! (I sometimes wonder if the power of Objective-C was C-abusing serendipity or stuff like this was planned.)  Swift doesn’t care much for serendipity. Let’s build the same protocol in Swift.


protocol ExampleDelegate {
	func numericValueForObject(object: DelegatingObject) -> Int
	func objectShouldDoMoreWork(object: DelegatingObject) -> Bool
}

extension ExampleDelegate {
	func numericValueForObject(object: DelegatingObject) -> Int {
		return 0
	}
	func objectShouldDoMoreWork(object: DelegatingObject) -> Bool {
		return false
	}	
}

The DelegatingObject would then have to use its delegate like so:


class DelegatingObject {
	func stuff() {
		var numericValue = delegate?.numericValueForObject(self)
		if numericValue == nil {
			numericValue = 0
		}
		...
	}	
}

The DelegatingObject can’t abuse the whole zero/NO/nil thing – if it asks its nil delegate for its numericValueForObject, it’ll get back an optional none instance. This isn’t desirable – in order to get the default behavior, the delegating object must duplicate the default behavior at the call site and in the extension. This is a bug waiting to happen. However, we can solve this problem by having the delegating object implement the delegate protocol in an extension.


extension DelegatingObject : ExampleDelegate {}

It’s a short line, and it is weird, but consider this: we’ve added the default behavior to the delegating object. Conceptually, this is no different than the Objective-C protocol approach. (I’d argue this Swift approach is even cleaner.) This allows us to change the call site in Swift to default back to its own implementation in the event of not having a delegate:


class DelegatingObject {

	func stuff() {
		var numericValue = (delegate ?? self).numericValueForObject(self)
		...
	}	
}

The default behavior is now described just once, and no extra types or objects have been created. More importantly, the world outside of the DelegatingObject is consistent: the delegate property is still nil, even though internally, there will always be an instance to evaluate these method calls. (Contrast this to a solution where the delegate property is set to self or some default object; an external observer would see that the delegate property as populated.)

Again, to the astute reader, there is still a problem here: what if a protocol has both required and optional methods? While rare, this is something that happens – UITableViewDataSource is an example. In the case of this extension approach, mixing in required methods causes an issue. If the extension implements a method, it is technically no longer required – there is a default implementation and implementors can omit it. If the extension omits the implementation, then we have to implement those required methods in the delegating object – which doesn’t make sense and it’s a bunch of extra code that’ll never get executed.

To solve this issue, we can split the required and optional methods. Required methods go in one protocol, optional go another. The good news is that the default implemented optional methods can be added to the required protocol:


protocol AProtocolOptional {
	func optionalMethod()
}
extension AProtocolOptional { 
	func optionalMethod() {
		...
	}	
} 

protocol AProtocol: AProtocolOptional {
	func requiredMethod()
}

(Another approach is to use protocol composition for the required and optional protocols to form a full protocol, instead of having AProtocol inherit from AProtocolOptional.) Now, the extension stuff only deals with the optional part of the full protocol. There is one oddly unnecessary typecast to use, but using AProtocol looks like this:


class DelegatingObject {
	var delegate: AProtocol?
	
	func doSomeThings() {
		var opt = (delegate ?? self as AProtocolOptional).optionalMethod()
		var req = delegate?.requiredMethod()
	}
}

extension DelegatingObject: AProtocolOptional {}

If you don’t like the typecast, you can move that bit of code into a private, computed property which can correctly infer the type without a cast:


class DelegatingObject {
	var delegate: AProtocol?
	private var innerDelegate: AProtocolOptional {
		return delegate ?? self 
	}
	
	func doSomeThings() {
		var opt = innerDelegate.optionalMethod()
		...
	}
}

*I say that adding optional methods to pure Swift protocols is never going to happen, but that isn’t necessarily true. It may be unlikely in Swift. We had an internal discussion on why this was the case. It could technically be possible by allowing implementations within the protocol declaration, which would be syntactic sugar for an extension. The compiler would have to relax the restriction on extensions to allow for partially implemented protocol extensions. If the compiler is enforcing that protocol conformers have all methods implemented, it should be able to check if all methods are implemented by joining the class and protocol extension. A protocol could then be partially extended because the rest of the program guarantees the protocol to be implemented by virtue of all conformers being checked for full implementation. We’re not sure if this is possible.

Joe Conway

Founder at Stable Kernel

Leave a Reply

Your email address will not be published. Required fields are marked *