inessential by Brent Simmons

Swift Diary #3: Arrays, Protocols, and Pointer Equality

Let’s say I’m writing a Finder replacement. (Disclaimer: previous statement may or may not be true.)

The common representation of the file system in a GUI app is a tree. A common pattern, then, is to create a tree data structure. The tree is made of Node classes, and each Node has a representedObject which conforms to the NodeRepresentedObject protocol.

A representedObject may be a Folder or a File. All the Node class knows is that its representedObject conforms to NodeRepresentedObject.

Each Node has zero or more children. The children are Node objects also. The children’s representedObjects are a mix of Folders and Files.

* * *

Here’s the trouble I ran into. Given a representedObject and an array of Nodes, I wanted to find the Node with that representedObject.

Things to know: the representedObjects are uniqued (a file system folder has one Folder object; a file system file has one File object). And, because they’re uniqued, they’re also objects rather than structs.

Because this is true, I can use pointer equality on representedObjects.

The Objective-C code might look like this:

- (Node \*)existingChild​WithRepresentedObject:​(id<NodeRepresentedObject)​representedObject {
  for (Node \*oneNode in self.children) {
    if (oneNode.representedObject == representedObject) {
      return oneNode;
    }
  }
  return nil;
}

Pretty simple.

* * *

My first go at doing this in Swift looked like this:

func existingChild​WithRepresentedObject​(representedObjectToFind: NodeRepresentedObject) -> Node? {
  for oneChild in children {
    if oneChild.representedObject === representedObjectToFind {
      return oneChild
    }
  }
  return nil
}

This didn’t compile: I get the error error: binary operator '===' cannot be applied to two NodeRepresentedObject operands.

“But,” I thought, ”shouldn’t pointer equality always work?”

Well, no. Not if representedObject is a struct.

So it occurred to me that I have to tell Swift that these are both objects, and it works:

if (oneChild.representedObject as! AnyObject) === (representedObjectToFind as! AnyObject)

* * *

But I don’t like using as! — as Rob Rhyne writes, “But every time you type !, someone at Apple deletes a Radar.”

So I figured there must be a better way. And there is.

  1. Keep the original line as it was: if oneChild.​representedObject === representedObjectToFind.

  2. Make NodeRepresentedObject conform to the AnyObject protocol, which lets the compiler know that representedObject is an object, so that pointer equality testing will work:

protocol NodeRepresentedObject: AnyObject {

It works. See the gist.

* * *

Here’s the part I don’t totally like: to make this work, it required bringing in Objective-C. AnyObject is an @objc protocol.

So here’s my question: is there a way to do this without bringing in Objective-C?

(I feel like there are two Swifts. Swift + Objective-C is like writing Objective-C with a new syntax. Pure Swift is another thing. I’m pragmatic, and I’ll use the former as needed — but I want to learn the pure Swift solutions.)

* * *

Update 10:15 am: the answer came quite quickly from Matt Rubin on Twitter and others:

try: protocol NodeRepresentedObject: class { … }

Done! It works.

* * *

Update 10:40 am: and there’s also this, which I didn’t know, from Joe Groff on Twitter:

AnyObject is marked ‘@objc’ for obscure implementation reasons, but it is a native Swift feature.