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:



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.

Posted on Aug 7
Written by Wayne Hartman