How do @optional protocol methods work, and how do you write a delegate in Swift that implements only some of them?

4.9.7 · L3 · Interview

Predict first

Two delegate classes, one implementing the optional method and one not. What does this print?

// Objective-C: @protocol DownloadDelegate  - (void)didStart;  @optional - (void)didProgress:(double)f;
// Downloader.run calls didStart, then didProgress: only if respondsToSelector
final class Plain: NSObject, DownloadDelegate { func didStart() { print("start") } }
final class Rich: NSObject, DownloadDelegate {
    func didStart() { print("start") }
    func didProgress(_ fraction: Double) { print("progress", fraction) }
}
let d = Downloader(), p = Plain(), r = Rich()
d.delegate = p; d.run()
d.delegate = r; d.run()
  • start / start / progress 0.5
  • start / progress 0.5 / start / progress 0.5
  • a crash for Plain

Mental model. An optional method is a form with some boxes marked “fill in if you wish”. The runtime checks the box before it reads the answer.

Objective-C protocols can mark methods @optional, and the caller checks before sending: [delegate respondsToSelector:@selector(didProgress:)]. Swift has no optional requirements in ordinary protocols, only in Objective-C protocols: @objc protocol Feed { @objc optional func refresh() }. An imported Objective-C protocol already has them, so a Swift class that conforms (it must inherit NSObject) can implement just the required methods and leave the rest out. Calling an optional member from Swift is written with optional chaining: delegate?.didProgress?(0.5).

The dispatch is dynamic, so the check is a real respondsToSelector: at runtime. That also explains the limits. The conformer must be an Objective-C-compatible class, the members must be exposed with @objc, and a misspelled selector (the Swift label has to match the Objective-C one) means the method silently never runs. Modern Swift code prefers a normal protocol with default implementations in an extension, or closures, and keeps optional methods for interop with UIKit and other legacy delegates. Delegates are normally weak, which only works because they are class-bound.

The Objective-C side asks the delegate whether it responds to didProgress: before sending it, so Plain is skipped without any error.

Common traps

  • Mistyping a delegate method’s label or argument type. The class still compiles (the method is just an ordinary one), and the framework never calls it.
  • Making the delegate a strong property. The delegate usually owns the owner, which makes a retain cycle.

They might follow up with

  • How do you give a Swift-only protocol optional behaviour? Provide a default implementation in a protocol extension.
  • Can a struct conform to an @objc protocol? No; the protocol requires a class.

Related questions

  • 4.3.1 What does NSObject provide, and when should a Swift class inherit from it?
  • 4.4.1 What do @objc, @objcMembers, @objc(name), dynamic and @nonobjc each do?
  • 4.1.1 What happens when you send a message in Objective-C, and what if nothing responds?

← Are Objective-C block parameters escaping in Swift, and what does…   How does an Objective-C completion-handler method become an async… →