Monthly Archives: October 2017

Selective Selector Mapping

I ran into an interesting challenge while porting some Objective-C code to Swift. The class in question served both as an NSTableView delegate and data source, meaning that it implemented methods both for controlling the table view’s behavior and for supplying its content.

Historically in Cocoa, most delegate relationships were established as informal protocols. If you wanted a particular class to be a table view data source, you simply implemented the required methods. For example, to populate a cell based table view, a data source would implement various methods, including one to indicate how many rows the view should have:

- (NSInteger) numberOfRowsInTableView:(NSTableView *)tableView;

In recent years, Apple has increasingly converted these informal protocols to formal Objective-C protocols. These give the compiler the opportunity to generate errors if a particular class declares compliance, but neglects to implement a required method. At runtime, however, the compliance-checking is still pretty loose. NSTableView consults its data source, checks to see that it implements a required subset of methods, and dynamically dispatches to them if it does.

The dynamic nature of NSTableView hasn’t changed with Swift. An @objc class in Swift that complies with NSTableViewDataSource must still implement the required methods such that Apple’s Objective-C based NSTableView can dynamically look up and dispatch to the required delegate methods. Swift’s method rewriting “magic” even ensures that a delegate method can be written in modern Swift style, yet still appear identically to older Objective-C code:

class MyDataSource: NSObject {
	@objc func numberOfRows(in tableView: NSTableView) -> Int {
		return 0
	}
}

Given an instance of MyDataSource, I can use the Objective-C runtime to confirm that a the legacy “numberOfRowsInTableView:” selector is actually implemented by the class above:

let thisSource = MyDataSource()
thisSource.responds(to: Selector("numberOfRowsInTableView:")) // false

Or can I? False? That’s no good. I’m using the discouraged “Selector” initializer here to ensure I get Swift to look for a very specific Selector, even if it doesn’t appear to be correct to the Swift-adapted side of the runtime.

I was scratching my head, trying to figure out why Objective-C could not see my method. Did I forget an @objc marker? No. Did I forget to make MyDataSource a subclass of NSObject? No. I finally discovered that I could second-guess the default Swift selector mapping to obtain a result that “worked”:

class MyDataSource: NSObject {
	@objc func numberOfRowsInTableView(_ tableView: NSTableView) -> Int {
		return 0
	}
}

let thisSource = MyDataSource()
thisSource.responds(to: Selector("numberOfRowsInTableView:")) // true

Instances of MyDataSource will get the job done for Objective-C calls to “numberOfRowsInTableView:”, but I’ve lost all the pretty formatting that I expected to be able to use in Swift.

There’s something else I’m missing out in my Swift implementation: type checking of MyDataSource’s compliance with the NSTableViewDataSource protocol. Old habits die hard, and I had initially ported my class over with an old-fashioned, informal approach to complying with NSTableViewDataSource: I declared a plain NSObject that happens to implement the informal protocol.

It turns that adding that protocol conformance onto my class declaration not only gains me Swift’s protocol type checking, but changes the way key functions are mapped from Swift to Objective-C:

class MyDataSource: NSObject, NSTableViewDataSource {
	func numberOfRows(in tableView: NSTableView) -> Int {
		return 0
	}
}

let thisSource = MyDataSource()
thisSource.responds(to: Selector("numberOfRowsInTableView:")) // true

Armed with the knowledge that my class intends to comply with NSTableViewDataSource, Swift generates the expected mapping to Objective-C. Notice in this final case, I don’t even have to remember to mark the function as @objc. I guess when Swift is creating the selector mapping for a function, it does so in a few phases, prioritizing more explicit scenarios over more general:

  1. First, it defers to any explicit annotation with the @objc attribute. If I tag my “numberOfRows…” func above with “@objc(numberOfDoodads:)” then the method will be made available to Objective-C code dynamically looking for “numberOfDoodads:”.
  2. If there’s no @objc specialization, it tries to match function implementations with declarations in superclasses or protocols the class complies with. This is what gives us the automatic mapping of Swift’s “numberOfRows(in:)” to Objective-C’s “numberOfRowsInTableView:”.
  3. Finally it resorts to a default mapping based on Swift API Design Guidelines. This is what yielded the default “numberOfRowsIn:” mapping that I first encountered.

This is an example of a Swift growing pain that is particularly likely to affect folks who are adapting older source bases (and older programming mindsets!) to Swift. If you run across a completely vexing failure of Objective-C to acknowledge your Swift class’s protocol compliance, start by making sure that you’ve actually declared the compliance in your class declaration!