Rant: Swift, Cocoa, Target–Action, and Instance Property Initialization

As someone who spends a lot of time hanging out on Stack Overflow, I get to see first hand what traps iOS Cocoa programmers fall into. In fact, that’s why I spend a lot of time hanging out on Stack Overflow. It’s fun and instructive. But it also pains me to see people making the same mistake repeatedly.

This is a case in point. People keep making the same mistake and it’s driving me nuts — because I think Swift could help out by not permitting this mistake in the first place. I do try to keep my frustration under control, but every once in a while, it bubbles up and needs an outlet, and this is it. In order to explain the problem, I have to take you back in time to explain how we got to now. Queue the flashback music, please…

Selectors — The Old Way

Back in the old Objective-C days, a frequent complaint was: “My app crashed with an unimplemented selector exception.” Usually, this had to do with the target–action pattern. In this pattern, which is extremely common in Cocoa, you specify an object (the target) and a message to be sent at some future time to that object (the action). Usually, this is your way of describing what should happen when the user does something in the interface, such as tapping a button.

To specify the action in the Cocoa target–action pattern, you use a selector — in effect, the name of the message. Objective-C is so dynamic that you can tell it the name of a message, and it can later construct an invocation of that message and call it on an object.

The problem was that specifying the action correctly was entirely up to you. If you specified the action incorrectly, your code would compile, but then at runtime, when Cocoa actually tried to send that message to that object, you’d crash.

For example:

UIBarButtonItem* hider = 
    [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"Key.png"] 
        style:UIBarButtonItemStylePlain 
        target:self 
        action:@selector(toggleEnglish)];

That code effectively assumes that self, which in this case is a view controller, has implemented a method called toggleEnglish. There was no checking by the Objective-C compiler, though — because Objective-C is so dynamic — so there might be no such implementation. For example, this is not an implementation matching that selector:

- (void) toggleEnglish: (id) sender {
    // ... do stuff here ...
}

The problem here is that the way to refer to that method using a selector expression is not to say toggleEnglish. Instead, you have to say toggleEnglish:, with a colon at the end. Get this wrong, and a crash is likely in your future when the user taps that button. The selector is translated into an invocation, the invocation is called, the message is sent, and it turns out that self hasn’t implemented any toggleEnglish method. Kaboom. Unimplemented selector, get it?

Swift To The Rescue, Sort Of

When Swift first came along, things were even trickier. You could still crash this same way, plus selectors are a purely Objective-C feature — so now there were all these new Swift programmers who didn’t know Objective-C, so they didn’t know anything about how to form the selector. They could easily write it incorrectly:

let hider = 
    UIBarButtonItem(image: UIImage(named:"Key"), style: .plain, 
                    target: self, action: Selector("toggleEnglish"))

Meanwhile, our method looks like this:

@objc func toggleEnglish(_ sender : Any) {
    // ... do stuff here ...
}

Well, that’s still not the right selector, so we’re still probably going to crash. And it’s even harder to get this selector right because if you don’t know Objective-C, you’ll probably never think to add that final colon to the name of the selector.

So the Swift Powers That Be decided to intervene. They asked themselves: “So exactly why can’t the compiler be more helpful in this situation?” Great question. Swift is static, not dynamic. The compiler knows perfectly well what methods are implemented here. And it can be taught the rules for forming a selector based on the method declaration — and it is a lot less likely to make a mistake about this than a human is!

So the Powers That Be came up with the Swift #selector syntax, and now we can write that same code this way:

let hider = 
    UIBarButtonItem(image: UIImage(named:"Key"), style: .plain, 
                    target: self, action: #selector(toggleEnglish))

The great thing about #selector syntax is that we don’t have to worry about forming the selector at all. The compiler does it for us. It looks at the #selector expression, sees that, even though we said toggleEnglish, we’re talking about the toggleEnglish(_:) method, and writes the correct Selector for us! Problem solved.

Except… Not really.

It turns out that the compiler isn’t actually thinking intelligently about this action in relation to this target. In fact, it isn’t thinking about who the target is at all. It’s just confirming that the #selector expression refers to an actual method that is exposed to Objective-C. It doesn’t care whether this is a method of the target.

To demonstrate, consider the following rather artificial example:

let spacer = 
    UIBarButtonItem(barButtonSystemItem: .flexibleSpace, 
    target: nil, action: nil)
let hider = 
    UIBarButtonItem(image: UIImage(named:"Key"), style: .plain, 
    target: spacer, action: #selector(toggleEnglish))

That compiles just fine. But do you see the mistake? The target in the second line is spacer. That’s not going to work. toggleEnglish is a method of self, not a method of spacer. Once again, a crash is in the offing.

So the Swift compiler is helping, but it isn’t helping quite enough. And that’s bad because we programmers, being human, tend to imagine that because the compiler prevents us from compiling under some situations where things will go wrong, it will prevent us from compiling under all situations where things will go wrong.

And it doesn’t.

All of which brings me, at long last, to the point.

A Subtle Antipattern

A common pattern in iOS programming is that you instantiate a view in code. You’re likely to do that, especially if you’d prefer not to use storyboards. Now, if you’re going to be needing a reference to this interface object generally, you might want to store that reference in an instance property. That’s also a common pattern. And so (you think to yourself) you might as well create and configure the interface object as part of the initialization of the instance property. Like this, for example:

class ViewController: UIViewController {
    let hider = 
        UIBarButtonItem(image: UIImage(named:"Key"), style: .plain, 
        target: self, action: #selector(toggleEnglish))
    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.rightBarButtonItem = self.hider
    }
    @objc func toggleEnglish(_ sender: Any) {
        print("yes")
    }
}

There is a bug hidden in that code. Can you spot it? It’s not easy. The code compiles. It runs. It doesn’t crash when the user taps the button. But here’s the problem: it doesn’t actually work, either. The user taps the button, and nothing happens! The toggleEnglish method is never called.

That’s the problem I’m here to rant about! This is a terribly insidious issue. The programmer appears to have done everything right. The #selector syntax compiles. The toggleEnglish method does exist, and it exists on self, the view controller. The code runs without crashing. There is absolutely no signal that there’s an issue.

But there is an issue, and my rant is that compiler should know there is an issue and should warn you about it.

The trouble stems from the use of the word self as the target. Think about where this code is. We are declaring an instance property, hider. It is an instance property of self, the view controller. We are initializing this instance property to be a bar button item that we are creating. That bar button item has a target, namely self. But wait! You can’t refer to self when you’re initializing an instance property — self isn’t initialized yet. It is the very thing we are in the middle of initializing.

In simpler situations, the compiler does actually prevent you from saying self when you’re initializing an instance property directly. The classic example is an instance property that depends on another instance property:

class ViewController: UIViewController {
    let firstName = "Snidely"
    let lastName = "Whiplash"
    let fullName = firstName + " " + lastName // error
    // ...
}

That code doesn’t compile. There’s an error, and the error message says: “Cannot use instance member firstName within property initializer; property initializers run before self is available.” Got that? Property initializers run before self is available. Makes sense. Okay, so what’s the difference between that and the initializer of hider? There isn’t much difference. One might argue, in fact, that this is exactly the same kind of mistake — but the compiler doesn’t catch it.

I’ve had a bug filed on this issue for years. I don’t know whether the Swift folks don’t believe this is a common pattern, or don’t believe it’s a bug, or don’t have a good way to teach the compiler to catch the potential issue. (Compiler intelligence is harder than it looks.) All I know is that nothing has been done about it. And so programmers keep making this mistake, and they keep being mystified about why their buttons (or bar button items, or timers, or gesture recognizers, or whatever it is they are initializing as an instance property with a target and action) do nothing.

You may ask why there is no crash when the button is tapped. I’m not entirely clear about that myself. It appears that self is taken to refer in this context to the class, which makes a certain sense. But the class has no such method, so if the message is being sent to the class, I still don’t get why there is no crash. It might have something to do with the fact that instance methods in Swift are curried class methods, but that’s just a guess.

So What’s the Solution?

At this point, you might be wondering: What’s the solution to this kind of bug? The thing the original code is trying to do is a perfectly reasonable thing to do, so how should we do it?

Answer: Use lazy.

Our original code looks like this:

let hider = 
    UIBarButtonItem(image: UIImage(named:"Key"), style: .plain, 
    target: self, action: #selector(toggleEnglish))

Rewrite it like this:

lazy var hider = 
    UIBarButtonItem(image: UIImage(named:"Key"), style: .plain, 
    target: self, action: #selector(toggleEnglish))

That works. The code compiles and runs and doesn’t crash and actually works — when the user taps the button, toggleEnglish is called. The reason is that by marking the property lazy, we ensure that its initializer doesn’t run until the first time the instance property is referred to by other code. By that time, the instance itself (self) has come fully into existence — and so self means the instance, which exists, and all is well.

(I’m sorry there’s no lazy let, but that’s another rant for another day.)

Another Solution, Sort Of

If you’re very savvy and have been keeping up with iOS 14 innovations, you might be wondering whether the new iOS 14 UIActions can help us here. To recap, new in iOS 14, you can assign to a UIControl — or to a UIBarButtonItem, because it is button-like — a UIAction whose handler: function will be called as the action. This effectively replaces the target–action architecture. Or rather, it hides the target–action architecture; there is still a target and an action, under the hood, but the target is not a separate object, and the action is now attached to the control (or bar button item) itself.

Can this feature save us from ourselves? Well, yes and no. This works:

let hider =
    UIBarButtonItem(primaryAction: UIAction(title: "", image: UIImage(named:"Key")) { _ in
        print("yes")
    })

There is no target self, so we can’t make the mistake of creating a button that doesn’t work when the app runs, which is what I’ve been ranting about. However, that particular example compiles only because the anonymous function happens not to contain any references to self. If it did, our code wouldn’t compile:

let hider =
    UIBarButtonItem(primaryAction: UIAction(title: "", image: UIImage(named:"Key")) { _ in
        self.doSomething() // compile error
    })

The solution is the same as before: make this property lazy:

lazy var hider =
    UIBarButtonItem(primaryAction: UIAction(title: "", image: UIImage(named:"Key")) { _ in
        self.doSomething()
    })

I do have to admit that this approach, in this case, is better. The compiler did save us from ourselves this time, in a sense; it didn’t let us misuse self. However, it’s not a universal solution. The problem is that there still lots of other types of object you might initialize in an instance property that do not take a UIAction; you still have to use the target–action architecture. UIGestureRecognizers are a frequent case in point. So, sorry, Apple, nice try, but I don’t retract my rant.


NOTE: The Secret Life of Swift Instance Methods

In Swift, instance methods are actually static/class methods. This is legal (but strange):

class MyClass {
    var s = ""
    func store(_ s:String) {
        self.s = s
    }
}
let m = MyClass()
let f = MyClass.store(m) // what just happened!?

Even though store is an instance method, we are able to call it as a class method — with a parameter that is an instance of this class! The reason is that an instance method is actually a curried static/class method composed of two functions — one function that takes an instance, and another function that takes the parameters of the instance method. After that code, f is the second of those functions, and can be called as a way of passing a parameter to the store method of the instance:

f("howdy")
print(m.s) // howdy

You Might Also Like…

The iOS Developer’s Guide to Updating an App to iOS14

Each year, like clockwork, for better or worse — sometimes, in my opinion, very much for worse — Apple releases a new version of iOS. We’re all used to the forced march of the calendar by now. In June, WWDC is held, and the new version appears in beta. In September, it goes final. And …

The iOS Developer’s Guide to Updating an App to iOS14 Read More »

    Sign Up

    Get more articles and exclusive content that takes your iOS skills to the next level.