Friday, 10 August 2012

You're Doing It Wrong #4: -[UIView viewWithTag:]

UIView has a tag property and a corresponding -viewWithTag: method. Used together, one can quickly access a specific subview without the need for an explicit property to reference that view.

I’ve seen UIView’s tag functionality abused in many strange and wonderful ways; so much so that I currently believe it best to avoid completely.

Do not use tags to store data

Let’s start with a ground rule:

UIView’s tag property should only be used as a unique identifier for a view. It should not store any more information than this.

Seems reasonable doesn’t it? And yet I’ve seen many developers abusing tags to store additional information, such as array indices pointing to model objects:

- (void)configureThumbnailButton:(UIButton *)thumbnailButton
                        forPhoto:(NSUInteger)photoIndex
{
    // ...
    thumbnailButton.tag = 1 + photoIndex;
    // ...
}

- (void)thumbnailButtonTapped:(id)sender
{
    UIButton *thumbnailButton = sender;
    NSUInteger photoIndex = thumbnailButton.tag - 1;
    id selectedPhoto = [self.photoArray objectAtIndex:photoIndex];
    [self showPhoto:selectedPhoto];
}

This example is setting up a button which displays a thumbnail for a photo. When the user taps the button, the corresponding photo is displayed.

To remember which photo should be displayed when the button is tapped, the tag property is given a value based on the array index of the photo. It’s common to see a magic number (1 in this case) added to the tag value to disambiguate between views with the default tag value of 0 and a genuine array index.

Using tags like this is pretty nasty. Although I’ve tried to make the above example as clear as possible for demonstration purposes, code that uses tags in this way is often very difficult to follow because there are no useful variable names anywhere.

Let’s take a look at some real-world examples:

[pageScroller scrollRectToVisible:CGRectMake(1024 * (sender.tag - 101),
                                             0,
                                             1024,
                                             pageScroller.height)
                                             animated:YES];

Another?

NSString *name =
((UITextField *)[((UITableViewCell*)[self.view viewWithTag:i]).contentView viewWithTag:2]).text;
[[[scrollView viewWithTag:100 + currentPage + 2] viewWithTag:22] removeFromSuperview];
homeButton.tag = (int)@"home";

Yes folks, these all come from real production code. Did I mention I love my job?

The correct way to associate extra information with a view

When a developer writes code like this:

NSUInteger photoIndex = thumbnailButton.tag - 1;

What he really wants to write is this:

NSUInteger photoIndex = thumbnailButton.photoIndex;

Or even:

Photo *selectedPhoto = thumbnailButton.photo;

The way to achieve this is obvious. You can create a custom subclass with additional properties to store the extra information you need.

In this case we have a button which is associated with a given photo, so we create a UIButton subclass and add a property to store the associated photo index, or, even better, a reference to the Photo object itself:

@interface PhotoThumbnailButton : UIButton

@property (nonatomic, assign) NSInteger photoIndex;
// OR
@property (nonatomic, strong) Photo *photo;

@end

Now when we want to get or set the associated photo for a button, we just use these public properties. Our code will be much clearer than if we had used tags and array indexes and weird additions and subtractions.

In some cases, creating a subclass is not possible, or it may seem a little overkill given your circumstances. In these cases, you can achieve similar improvements to your code by making use of Objective-C associated references.

Associated references

Associated references make use of objc_setAssociatedObject and objc_getAssociatedObject to attach values to an existing object at runtime.

In our case we can write a couple of helper methods like this:

#import <objc/runtime.h>

static char kThumbnailButtonAssociatedPhotoKey;

// ...

- (void)setAssociatedPhoto:(Photo *)associatedPhoto
        forThumbnailButton:(UIButton *)thumbnailButton
{
    objc_setAssociatedObject(thumbnailButton,
                             &kThumbnailButtonAssociatedPhotoKey,
                             associatedPhoto,
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (Photo *)associatedPhotoForThumbnailButton:(UIButton *)thumbnailButton
{
    return objc_getAssociatedObject(thumbnailButton,
                                    &kThumbnailButtonAssociatedPhotoKey);
}

Now we can easily set/get the associated photo for a button:

- (void)configureThumbnailButtonForPhoto:(Photo *)photo
{
    // ...
    [self setAssociatedPhoto:photo
          forThumbnailButton:thumbnailButton];
    // ...
}

- (void)thumbnailButtonTapped
{
    Photo *photo = [self associatedPhotoForThumbnailButton:thumbnailButton];
    // ...
}

So with a couple of short helper methods, we can easily attach model objects to our UI controls. Associated references are very cool.

The only (semi-)acceptable way to use tags

Let’s assume you aren’t trying to store data in a view’s tag. Instead, you just want a quick and dirty way to grab a reference to a view. Is it OK to use tags in these situations?

Well, in almost every case I can think of, it is better to store a reference to the view using a real property somewhere, whether that’s an IBOutlet or just a regular property on your class.

Do you need to add some custom views to a UITableViewCell? Subclass it and add real properties. Need to know which of many UIAlertViews is calling your alertView:clickedButtonAtIndex: method? Assign the alerts to properties when you create them, and compare the pointers directly.

By using real properties you get stronger typing, better naming, better visibility of the moving parts of your app, and you don’t have to down-cast viewWithTag’s UIView* return type. You also get better performance because viewWithTag: must traverse the view hierarchy for every call.

To me, using tags seems to be another pattern driven purely by laziness. Adding a property to your parent object is not difficult, especially now that we’ve all upgraded to ARC and Xcode 4.4, where -dealloc and @synthesize are unnecessary in most cases. You have upgraded, haven’t you?

But mooooom…

Oh alright then. If you do decide to use tags, you must follow our ground rule and only store unique identifiers for your views. Furthermore, we can say:

UIView’s tag property should only be given a named #define or enum value.

For example, this is good:

enum MyViewTags {
    kTitleLabelTag = 1,
    kSendButtonTag,
    kSomeOtherViewTag
};

// ...

if (sender.tag == kSendButtonTag) {
    // ...
}

The last thing we want are magic numbers flying around in our code. Always create named constants for your tag values, otherwise you end up with this kind of stuff:

aiView.tag = 22;
content.tag = 111;
globalSyncAlert.tag = 1000;
nextZoomingView.tag = 2000;
infoView.tag = 3000
white.tag = 1111;
howto.tag = 4357824;
imageView.tag = 199;
self.tag = 999;
label.tag = 3542
homeVC.view.tag = 1234;
b.tag = 1983;

Again, these are all real-world examples. I like to collect them.

Summary

In summary, try to avoid using UIView tags as much as possible. Follow these rules:

  • Don’t store data in a view’s tag. Prefer subclassing and Objective-C associated references to associate additional information with a view.
  • Prefer real properties and XIB outlets over tags for getting references to subviews.
  • If you do use tags, do not use magic numbers. Use named constants.

Follow these rules and your code will be much better off. Your fellow developers will thank you.