Why you should not name your @IBActions didTapButton
Often I see code like this:
class MyViewController : UIViewController {
override func viewDidLoad() {
// [...]
let button = UIBarButtonItem(barButtonSystemItem: .cancel,
target: self,
action: #selector(didTapCancelButton(sender:)))
}
@IBAction func didTapCancelButton(sender:Any) {
// cancel
}
}
And I really donât like it. The part that I donât like is the name of the action-method. More specifically, the didTap
-part. It might as well be didTapSaveButton
or didHitSend
. Or cancelButtonTapped
. All of those feel wrong⢠to me. There are two main reasons for that:
Target-Action is a Command Pattern, not a Delegate Pattern
Deep in Appleâs Developer Documentation, hidden in the âGuides and Sample Codeâ, you can find an article explaining the Model-View-Controller (MVC) pattern. 1 They explain that the original MVC-pattern is made up of the Composite, Strategy and Observer patterns and go on to explain that their version of MVC uses two additional patterns: the Mediator and the Command pattern. Target-Action is the Command Pattern part of Appleâs MVC.
In the Target-Action version of the Command pattern, the UI element (e.g. a button) is the invoker, the action
/message passed to/method called on the object is the command and the target
is the receiver. Now, a command should be something like undo
, deleteBackward
, lowercaseWord
, or scrollToEndOfDocument
. Those are all actions an NSResponder
understands and might (depending on the specific instance and other circumstances) react to. WKWebView
has reload
among others. UIResponder
is a little smaller but still has the classics: cut
, copy
, paste
, selectAll()
and of course toggleBoldface
, toggleItalics
, and toggleUnderline
. What did you think how the overlay menu you get for a text-selection works?
So Apple definitely calls their actions like commands, which seems like a good example to follow. In contrast, calling your action didTapXButton
sounds like a delegate method. But we are not using a delegate pattern here. We donât expect a call from some very specific source asking as for specific information like in a delegate pattern. Instead we get a command to interpret and execute. And this command may or may not be invoked. Or may be invoked multiple times. By different senders, we donât even know who will be triggering the action.
We donât know the sender at compile-time and the sender shouldnât be relevant
An IBAction can be invoked from any kind of control2. Your didTapSendButton
could be triggered when the user hits the return key on the keyboard after entering text in the message field. In a macOS app, I might trigger the sending of the mail from a menu item. Or maybe I tapped on a button in the TouchBar and not the one in your window. Or maybe by a triple tap gesture recognizer. It doesnât matter, the intention of the user is clear, they want to âsendâ whatever they were writing.
By adding the name of the UI element to the method name, you couple the controller to the view more closely than necessary. And should you ever need to know how the user invoked the action, e.g. because you want to track whether anyone really uses the TouchBar, you can still look at the sender
property and do some customization3. Thatâs what itâs for. But adding some customization for special cases and otherwise keeping your action-method generic makes it much more flexible to be triggered from UI elements you might not even have heard of when you initially wrote the code.
Except for some very specific commands, it shouldnât matter from where you action is invoked. lowercaseWord
means âmake the currently selected word lowercaseâ. copy
means store the currently selected data (text, image, whatever) in the clipboard. selectAll
means âselect everything I can see hereâ, whatever that everything is. And undo
means âI changed my mind, please roll back.â
Name Your Actions Like Commands
So Apple names their methods in this way, it fits the pattern and it decouples our controllers from our views. Whatâs not to like?
Thus, we should name our actions like commands. Make it cancel(sender:Any)
, save(sender:Any)
and sendMessage(sender:Any)
instead of didTapCancelButton(sender:Any)
, saveButtonTapped(sender:Any)
and didHitSend(sender:Any)
4.
And by the way, it doesnât matter whether you actually use Interface Builder or not. As long as you assign targets and actions to UI elements your action-methods should follow this naming pattern. I would even recommend to add @IBAction
in front of them, as itâs a nice marker to indicate to other programmers that this will be invoked from UI elements and they might not have full control over when and how and should thus not assume anything more than is provided by the action name.
-
I highly recommend reading it, independent of this blog post. ↩
-
And should you ever want to make your action only be invokeable by buttons, still name it in a generic way but make the type of sender e.g.
UIButton
. It doesnât help when you specify the action programatically, but Interface Builder will respect your choice and only offer it to be triggered by elements that fit that type. Yet another great reason to use Interface Builder. ;-)Â ↩ -
Did you know that you can add more than one action to a
UIControl
(sadly not toNSControl
, thanks to Mark Aufflick for pointing that out)? So instead of customizing thesend
action and adding your tracking there, just make the TouchBar element invoke the normalsend
action and add another actiontrackTouchBarUse
with yourTrackingController
as target. They will both be invoked when the user taps the TouchBar item. This way your view controller neither needs to know about tracking nor about how many different ways there are to invokesend
and you can simply configure the actions in Interface Builder. đ¤ŻÂ ↩ -
macOS makes this much clearer.
NSWindow
,NSDocument
and friends all have lots of great action-methods. This is why you can so often build pretty advanced macOS applications by just combining some instances of Cocoa classes in Interface Builder and configuring them. Sadly, UIDocument and itâs friends have forgotten most of that, but I think following this naming pattern still fits well with Cocoa Touch and has all of the advantages described above. ↩