THIS IS A BLOG POST DRAFT THAT I WILL NEVER PUBLISH
INSTEAD I PLAN TO DO A SERIES OF SMALLER BLOG POSTS ON THE SUBJECT
The Objective-C Runtime and Declarative Programming
I write new code in Swift. It’s fun and I like it.
But I also like Objective-C — well, not the language syntax, but what it can do.
If you’re writing iOS or Mac apps in Swift, you’re working in an Objective-C runtime world.
Learning about this will make you a better programmer, not least because it will help you understand the environment you’re working in. The more you learn about your environment, the more you can make it do what you want.
I’m going to talk about some Objective-C capabilities in the context of declarative programming. The good news is that many of these capabilities are also available to Swift (or, at least, the impure Swift we write when building Mac and iOS apps).
Why Declarative Code Is Good
Declaring things is often considered better than writing traditional code. If you declare something, you say what should happen, but not how. With traditional code, you’re writing the how.
Declarative code is often simpler and easier to maintain. In some cases it can be maintained by people who don’t write traditional code at all.
Consider the Storyboard
When you’re designing a UI with Interface Builder (meaning the UI designing part of Xcode), you’re declaring what should happen.
You’re declaring, for instance, that a button should be right there, with these layout constraints, and it should send this action to that target when tapped or clicked.
You’re not instantiating the button in code, or writing layout geometry code, or writing code to override mouseUp or whatever.
Interface Builder generates a textual representation of your interface. (In XML these days.) We used to call these “freeze-dried objects.” These objects are warmed-up and made real at runtime.
This was a major advance over other systems that would generate source code with a big warning that said “DO NOT EDIT” at the top of each file.
But here’s the thing: an XML file is just a big string. How does it go from this string to actual user interface objects?
Let’s say you told Interface Builder to use your
CoolButton class for your button. (It’s a subclass of
NSButton.) How in the heck does it actually make a
It’s definitely not magic. In earlier days of Cocoa programming, this used to be a mantra: “No magic.”
In other words, even though you don’t have the source code to the storyboard loader, you can at least conceptually understand how things work under the hood — and you could conceivably write that code yourself.
That’s important! The power is not reserved to a priesthood in Cupertino. It’s ours too.
So: the storyboard (or xib or nib) loading code has a string parsed from the XML file –
"CoolButton" — and it turns into an object of the correct type using code that conceptually looks like this:
Class buttonClass = NSClassFromString(buttonClassName); UIButton *button = [[buttonClass alloc] init];
Certainly the actual code that instantiates objects in storyboards is more complex, but the gist is the same. Given a string, you can get a reference to its class. Then, given the class, you can create an object of that class.
This is true even though the storyboard-loading code knows nothing about your code. It can look up your classes at runtime, so it works.
Wait… but… if the class is looked up at runtime, and it’s not there… blooey?
Yes. (For various types of blooey.)
In general this is the kind of bug that’s super-obvious the minute you build and run. It’s not caught by the compiler, but it’s caught by the developer easily and quickly.
Consider the trade-off: you get to write declarative code, using a visual user interface designer, and pay the price with a possible exception at runtime. All these apps all these years have been doing it. The trade-off has been well worth it.
(Maybe in the future there wouldn’t have to be that trade-off? Maybe! That would be cool. But if the price is that Interface Builder generates source code with a big DO NOT EDIT warning, then it’s not worth it.)
Hooking up the button
CoolButton button probably has a target and an action. The target was probably instantiated in a way similar to the button —
NSClassFromString was likely used.
But the action is a different thing. It’s a selector. A selector is kind of like a reference to a method, except minus the object performing the method.
Objective-C is a message-passing system: instead of calling functions directly, it passes messages. Thus
[shape bringToFront] isn’t a direct call — instead, it passes the
bringToFront message to
shape (the receiver of the message). The message-to-perform is itself a sort of object: a selector, or
And, similarly to
NSClassFromString, you can create a selector from a string: see
When the storyboard loader goes to wire up the button, it creates the selector with something like:
button.target = target; button.action = NSSelectorFromString(actionName);
actionName is a string from the XML file.)
(Again: yes, the actual storyboard-loading code is surely more complex than that, and I probably don’t have the correct button API. You get the point.)
You could set up the button in Interface Builder so that it sends a message that the target doesn’t actually handle, and then you’d get an exception at runtime. Yes.
But, again, hopefully you’ve clicked or tapped the button at least once before shipping!
You can see how the Objective-C runtime makes declarative programming possible. Here are three key things covered so far:
- Given a string that’s the name of a class, you can look up the class (via
NSClassFromString) and create objects.
- Because Objective-C is a message-passing system, you can specify objects and messages as separate objects.
- Given a string that’s the name of a message, you can create a selector (via
NSSelectorFromString) and send that message to any object.
Consider Core Data
The Objective-C runtime also makes Core Data possible. Here’s another place where you declare — you use the Core Data modeler rather than having to write it all in code.
You get and set properties on Core Data objects, in both Objective-C and Swift, using dot syntax — as in
feed.name = newName.
But remember that, because Core Data objects are Objective-C objects, this is still message-passing: you could write
[feed name] and
[feed setName:newName] instead.
Core Data objects all descend from
NSManagedObject, and there’s something special going on here. The methods to get and set these properties are created at runtime.
In Objective-C you mark these properties as
@dynamic, which tells the compiler that the method implementation will not be there during compilation but will be there at runtime. (This is something you could use for your own code, by the way — it’s not just for Core Data.)
In Swift you mark these as
@NSManaged, which tells the compiler something similar, but isn’t a general mechanism like
@dynamic. (There’s a special case going on here for Swift.)
When you run the app, NSManagedObject creates getter and setter methods for the various @dynamic (or @NSManaged) properties. (This works in Swift because the object is still an Objective-C object, even though your code is in Swift.)
(The actual mechanism for adding methods at runtime is beyond the scope of this article. It’s not pretty, but it’s not difficult, either — it’s just a matter of housekeeping, getting the details correct.)
Why does it do this? It’s because NSManagedObject is talking to a database. It knows whether or not an object is a fault — that is, whether its values have been fetched yet.
If you were implementing each getter method separately, they might look something like this:
NSString *propertyName = @"name"; [self fetchValuesFromDatabaseIfFault]; return self.fetchedValues[propertyName];
Repeat for each and every property. And make sure
propertyName is correct for each different property.
Instead, though, you can create methods at runtime — a getter and setter for each property. The getter would funnel to a single method that takes the property name, fetches from the database if needed, and then returns the value for that name.
For the setter, it records the change in in-memory storage, and notifies the
NSManagedObjectContext, which later writes to the database when it’s the right time.
(Note: this is how I’d do it, most likely. Of course I’ve never seen the Core Data source code. And of course it’s more complex than this illustration.)
Without the ability to add methods at runtime, Core Data wouldn’t be possible, and there’d be no Core Data modeler, and you’d have to write everything the hard way.
(Not totally true. You could use Key-Value Coding, as I think early Core Data did, but then you wouldn’t be able to refer to
feed.name and similar — you’d use
You see, I hope, how the runtime features of the Objective-C runtime make Core Data possible.
If you wanted to write something like Core Data, you could.
Example in the Wild #1: Vesper
It didn’t have faulting, so it wasn’t necessary to provide methods at runtime as Core Data does. It just used standard property access.
Instead of using the Core Data modeler, I defined the model using a property list, in Xcode’s property list editor. While not as nice as the modeler, it worked.
The system used another Objective-C runtime feature called Key-Value Coding. The database system needed some way to know about the objects it was getting and setting, and that knowledge was in the plist.
The database system then used
[object setValue:someValue forKey:propertyName] to set the properties on the database objects.
someValue came from the database, and
propertyName came from the plist.
Then, in higher-level parts of Vesper — the parts that knew about the data model — it would just get and set values naturally:
note.text = @"This is a note" and so on.
What I would have loved — but I never cracked — would have been to just define the data model in one place, completely declaratively. But Core Data hasn't cracked that yet either, and you still have to keep the modeler and the objects in sync.
Example in the Wild #2: Evergreen
My app supports unmodified keyboard shortcuts in a few different places. For instance, in the sidebar, the
, key collapses selected rows. The right-arrow key navigates to the timeline.
I could handle all this in code, in a big switch statement — but this gets messy and hard to modify, and can be a source of bugs.
Instead I created another plist format for defining the shortcuts. It maps keys to actions — and the actions are just strings like
navigateToTimeline:, and so on.
When the generic key-handling code finds a match, it looks up the string and makes a selector out of it (via
NSSelectorFromString), and then calls that method.
I could mis-type a selector name in the plist, and then there’s be an exception when that key is pressed. Yes, totally. But that’s the kind of thing that’s easy to test for — it’s obvious and not intermittent — and it’s worth it to have a simple way of maintaining the several lists of keyboard shortcuts.
But here’s the marvelous part: my generic keyboard shortcut handling is not written in Objective-C — it’s written in Swift. But because it’s an app, we have the Objective-C runtime available. The code looks like this:
let action = NSSelectorFromString(actionString) NSApplication.shared.sendAction(action, to: nil, from: view)
That second line is worth calling out: it sends an action as if
view is the top of the responder chain. First it sees if
view responds to that selector (via
respondsToSelector:), and if not it tries the next responder (often a superview or view controller), and so on until it gets all the way up to the application itself.
This means I can implement each keyboard handling method at the right place —
collapseSelectedRows:, an outline view command, may need to be handled by an object different from the one that handles
navigateToTimeline:, for instance.
Example in the Wild #3: Omni Apps
Lest you think I’m the only who does this kind of thing…
Long before I came to Omni, they developed a plist format for defining toolbars in their Mac apps. (See, for instance, Document.toolbar in the Resources folder inside the OmniFocus for Mac package.)
Toolbars can be configured entirely in traditional code — but that can get quite messy. They can also be configured in Interface Builder — but, well, that ends up being kind of heavyweight in this case where you just want some text.
So a plist is a pretty nice way to do it. Easy to maintain, easy to make changes.
For each toolbar item in the plist there’s an image name, action, and so on. The image name is a string that can be passed to
[NSImage named:], and the action is a string where, again, the apps creates a selector via
NSSelectorFromString and assigns that action to the toolbar item.
I’ve talked about a few features of the Objective-C runtime: looking up classes with a string, message passing, creating selectors with a string, adding methods at runtime, and Key-Value Coding.
Swift tries much harder than Objective-C to be safe by default. But these runtime things are still there — and, if you write an iOS or Mac app, you can’t avoid them: these features power storyboards, Core Data, event handling, and plenty more. Understanding this stuff is on the road to becoming a good app writer.
You won’t necessarily ever write your own declarative system (or use the runtime in other ways) — but I wouldn’t be surprised if you did, given its advantages. The trade-offs are often worth it.
The future may bring better and safer ways of declarative programming. That would be great! I’m all for it.
But what I don’t want is a future where the only declarative systems are provided by Apple magic — I want the freedom and power to create my own systems, too.
And so should you! You want safety for almost all of your code — but sometimes that extra bit of power is going to make your app easier to maintain and easier to extend. Use it judiciously, yes, but use it when you need it.
P.P.S Deleted from an earlier draft: “I often think of Objective-C as a liquid and Swift as a solid. Or: Objective-C is a full-grown cat, while Swift is a puppy.” Cats operate in three dimensions very effectively, but they refuse to play fetch. I don’t know what this means. :)