A Case For Using UIView Tags
UIView tags aren't the best tool to use in pretty much every situation, but they are useful in very specific situtations.
I have seen quite a few tweets floating around on Twitter that mock and deride the use of the tag
property on UIView
:
“UIView.tag is the Cheez Whiz of the programming world. It’s super fast and convenient, but it is no way ever good for you”
— Dave DeLong (@davedelong) August 4, 2016
A radar in which I suggest updating the UIView.tag documentation with “Not deprecated per se, but so help me god… that’s it go get my belt”
— Mark Adams (@hyperspacemark) April 5, 2016
Now, to be clear, I am not advocating that the tag
property sits in the Pantheon of CocoaTouch best practices, since it introduces a certain fragility to your code for the uses in which the API was originally developed. That is to say, viewForTag:
was created to traverse view hierarchies without having to have a pointer to them. These tags are usually set in Interface Builder and then used at runtime. Bad idea. Just create an IBOutlet
and move on.
But as I mentioned in a tweet today, though it seems popular at the time, I'm not sure that blanket calling them bad is a correct, either. I believe that there are a few benign uses of tag
that are useful and OK.
MapView Callout Accessories
Consider the following example with a MKMapView
, using standard UIButton
accessories in a callout:
The standard pattern here is to add a UIControl
instance as the left and/or right accessory of the callout. When a user taps on them, the map view delegate will be notified via the mapView:annotationView:calloutAccessoryControlTapped:
callback.
If we have two of the same kind of controls in these accessories, how do we differentiate between them? We could create a UIButton
subclass and set some model data, or even a closure to execute. Or, we could just use a vanilla button and use an easy to understand convention to control the flow of our application.
In the above example, I created an enum
to create a domain of acceptable tag values:
enum CalloutAccessoryType : Int {
case insert = 0
case delete = 1
}
Then, when I'm creating the buttons for the accessories of the annotations, I set the tag to one of those domain values:
let add = UIButton(frame: frame)
let delete = UIButton(frame: frame)
...
add.tag = CalloutAccessoryType.insert.rawValue
delete.tag = CalloutAccessoryType.delete.rawValue
Next, when the user taps on either of these buttons, the appropriate MKMapViewDelegate
callback is called, where I can switch off of the tag of the button:
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
if view.annotation!.isKind(of: RoutePoint.self) {
let accessoryType = CalloutAccessoryType.init(rawValue: control.tag)!
switch accessoryType {
case .insert:
// Insert an annotation
case .delete:
// Remove the annotation
}
}
}
This approach strikes a balance between arbitrary magic numbers scattered throughout your code and an over-engineered solution to know which buttons someone tapped on a callout: It leverages a property of an existing class without subclassing (ew!) and if there are domain values added or removed, there are compile-time checks that will be required to be addressed by the developer.