iOS
Control, Target, and Action in iOS 14
Matt Neuburg
Written on October 27, 2020

The target–action pattern is one of the oldest in Cocoa, and it’s used with some of the most important interface objects, namely controls (UIControl). That includes buttons (UIButton), switches (UISwitch), segmented controls (UISegmentedControl), and many others. It is also used with UIBarButtonItem because it’s button-like even though it isn’t a control (or even a view).
Here’s the basic pattern, enacted entirely in code by calling addTarget(_:action:for:)
, possibly in your viewDidLoad
implementation:
self.button.addTarget(self,
action: #selector(buttonPressed),
for: .touchUpInside)
That sort of thing requires that there actually be a method buttonPressed
in self
. For example:
@objc func buttonPressed(_ sender: Any) {
let alert = UIAlertController(
title: "Howdy!", message: "You tapped me!",
preferredStyle: .alert)
alert.addAction(
UIAlertAction(title: "OK", style: .cancel))
self.present(alert, animated: true)
}
Alternatively, instead of @objc
, we could write @IBAction
, and then instead of adding the target and action in code, we could configure an action connection in the storyboard. But it’s the same thing in the end; the action connection is merely a graphical way of writing (and, at nib-loading time, performing) a call to addTarget(_:action:for:)
.
The whole pattern here is extraordinarily archaic, clunky, and un-Swift-like. The selector points to a method that must exist separately for no other purpose than for the button to call it later, so you end up working in two places to configure the button. Some other objects that use the target–action pattern have modernized by allowing you to specify an anonymous function instead (NotificationCenter and Timer are obvious cases in point). So why not UIControl?
Many third-party attempts have been made to modernize UIControl in just that way so that you can hand it a function (a closure) that it will call internally. The problem of how to store that function in the control is a tricky one; solutions in the past have often revolved around objc_setAssociatedObject
(scary stuff). But in iOS 14, at long last, Apple has provided an official way to do this.
Lights, Camera, UIAction
Apple’s solution is to use the UIAction class as a wrapper for the function. Or perhaps I should say “misuse,” as UIAction is actually aimed at a different purpose altogether: it’s how you configure a menu item in a context menu. A UIAction has a handler (a function), and so you can use it to convey your function to the control. I’ll rewrite the preceding example to illustrate; there is now just one snippet of code in just one place:
let action = UIAction { [weak self] action in
let alert = UIAlertController(
title: "Howdy!", message: "You tapped me!",
preferredStyle: .alert)
alert.addAction(
UIAlertAction(title: "OK", style: .cancel))
self?.present(alert, animated: true)
}
self.button.addAction(action, for: .touchUpInside)
Or, defining the action inline:
self.button.addAction(UIAction { [weak self] action in
let alert = UIAlertController(
title: "Howdy!", message: "You tapped me!",
preferredStyle: .alert)
alert.addAction(
UIAlertAction(title: "OK", style: .cancel))
self?.present(alert, animated: true)
}, for: .touchUpInside)
Which of those syntaxes do I prefer? Neither! They’re not Swifty. Come on, Apple, closures should come at the end, so that you can use trailing closure syntax. Here’s an extension:
extension UIControl {
func addAction(for event: UIControl.Event,
handler: @escaping UIActionHandler) {
self.addAction(UIAction(handler:handler), for:event)
}
}
Now we can say this:
self.button.addAction(for: .touchUpInside) { [weak self] action in
let alert = UIAlertController(
title: "Howdy!", message: "You tapped me!",
preferredStyle: .alert)
alert.addAction(
UIAlertAction(title: "OK", style: .cancel))
self?.present(alert, animated: true)
}
Now that is Swifty.
Return to Sender
You may be asking: “Whoa, not so fast. With the target–action architecture, the control itself arrives as the parameter of the action method, conventionally under the local name sender
. With this new closure-based architecture, where’s my sender?”
It’s inside the action
parameter. As part of the “misuse” of UIAction, the UIAction class has gained a sender
property. So if you need the sender, that’s the place to look:
self.button.addAction(for: .touchUpInside) { [weak self] action in
let alert = UIAlertController(
title: "Howdy!", message: "You tapped a \(type(of:action.sender!))",
preferredStyle: .alert)
alert.addAction(
UIAlertAction(title: "OK", style: .cancel))
self?.present(alert, animated: true)
}
Special Control Powers
In the case of a UIButton or a UISegmentedControl, the UIAction architecture sprouts some extra wings. I’ll tell you about that now.
Earlier, I ignored the fact that a UIAction has some other properties besides its handler function. In particular, it has a title
property and an image
property. I pretended that those don’t exist. But they do exist, and a UIButton can pay attention to them if you provide them.
UIButton, in iOS 14, has a new initializer: init(type:primaryAction:)
. The type:
parameter is optional; if you omit it, it defaults to .system
. The really interesting part is the primaryAction:
. It’s a UIAction that will be attached automatically to the button’s .touchUpInside
event. But get this: in addition to setting the action function to be called when the button is tapped, the UIAction’s title:
is set as the button’s title for the normal state, and its image:
is set as the button’s image for the normal state!
So that’s a really fast, simple way to create a button with a title and an image and a .touchUpInside
function. Cool!
UISegmentedControl is even cooler. You get to specify a different UIAction for each segment! That’s a big improvement because it means you don’t have to examine the segmented control’s selectedSegmentIndex
to decide what to do.
Not only that, but the UIAction’s title:
or image:
can configure the segments themselves. Whichever you supply, the title or the image, is displayed in the segment’s interface. (If you supply both, the image wins.)
If you’re creating the segmented control in code, you can use the initializer init(frame:actions:)
. In this example, a single call to the initializer creates the segments, sets the title and image for each one, and gives each segment its own action handler:
let im = UIImage(named:"one")!.withRenderingMode(.alwaysOriginal)
let action1 = UIAction(image: im) { action in
print("segment zero selected")
}
let action2 = UIAction(title: "Two") { action in
print("segment one selected")
}
let seg = UISegmentedControl(frame: .zero, actions:[action1, action2])
You can also insert a segment as a UIAction, set an existing segment’s UIAction, and explore the relationship between segments and actions:
insertSegment(action:at:animated:)
setAction(_:forSegmentAt:)
segmentIndex(identifiedBy:)
actionForSegment(at:)
The Secret Life of UIAction
The target–action architecture is not going away. You don’t have to adopt the UIAction approach if you don’t want to. You can adopt it partially but not totally. You can mix and match: a single UIControl, even a single control event within a single UIControl, can have both UIAction and target–action dispatch.
Under the hood, a control is still a control. It is still, in a very real sense, using the target–action architecture even when you attach a function using a UIAction. There is still an underlying dispatch table, and it still works the same way it did. It’s just that when you use a UIAction, the target is some internal object. The UIAction stuff is an addition to the way a control works, but it doesn’t change anything about how a control works.
As part of this addition, there are some new methods for introspecting the dispatch table:
enumerateEventHandlers(_:)
removeAction(_:for:)
removeAction(identifiedBy:for:)
The first of those methods, enumerateEventHandlers(_:)
, is the key to introspecting the complete list of actions registered with the control’s dispatch table. You supply a function that takes four parameters: an action, a target–action pair (expressed as a tuple), a control event, and a pointer to a Bool (so that you can stop the enumeration by setting its pointee
to true
). The function is called for each event handler in the dispatch table. The parameters reflect how the event handler was added:
As a UIAction:
If this event handler was added as a UIAction, the first parameter will be the UIAction and the second parameter will be nil
.
As a target–action pair:
If this event handler was added as a target–action pair, the first parameter will be nil
and the second parameter will be a tuple consisting of the target and the action selector.
The third parameter is the control event under which the action is registered. Here’s a toy example for testing purposes:
button.enumerateEventHandlers { action, pair, event, stop in
print("got an action for", event)
if action != nil {
print("This one is a UIAction")
}
if pair != nil {
print("This one is a target–action and the target is", pair!.0)
}
}
Downsides and Upsides
There’s only one way to configure a control to have a UIAction in its dispatch table, and that’s in code. The storyboard isn’t going to help you here. If you want a control in the storyboard to have an action connection, that connection is going to be to a good old-fashioned @IBAction
method, and there’s nothing you can do about that.
Another unfortunate aspect of this change is that it isn’t universal throughout the entire range of classes that use the target–action architecture. A UIGestureRecognizer, in particular, still needs a target
and an action
.
Also, this change is not backward-compatible. It’s a feature of iOS 14, not a Swift language feature; you can only adopt it when your app is going to be running under iOS 14 or later.
And I have a handful of very nitpicky complaints:
-
I think Apple was wrong to misuse UIAction for this purpose; in my view, a new separate class (UIControlAction, maybe?) would have been better.
-
As I’ve already said, I think the syntax for adding an action to a control,
addAction(_:for:)
, where the first parameter is the UIAction, is really poor; it looks like it was devised by an Objective-C programmer who hasn’t made allowances for the fact that Swift even exists. -
Apple, you put in all this time and effort to let a UIControl take a function, and you still haven’t given a UIControl a
publisher
that can be used with the Combine framework? For shame!
Nevertheless, I hope I’ve made it clear that I think this is a pretty nice addition to the event architecture of Cocoa. The idea that there’s now an official way to avoid using the target–action architecture with UIControls makes me very happy, and I intend to take full advantage of it going forward.