Post

Associating Objects with a Class

Associated objects

I believe most people have seen the two methods objc_setAssociatedObject and objc_getAssociatedObject to some extent.

What are they actually useful for? If we only say “associated objects,” that does not really show their practical value. Here is a more practical use case. When using UIAlertView, have you ever felt it was cumbersome? The logic for button taps has to be written in delegate methods. If a VC contains multiple UIAlertView instances, things become even more complicated, because you have to determine which alertView object was passed into the delegate method. We can use the two methods above to solve this kind of problem once and for all.

In the end, creating and showing a UIAlertView can look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	[self showAlertViewWithTitle:@"Are you sure you want to close the case?"
                             message:nil
                   cancelButtonTitle:@"Cancel"
                   otherButtonTitles:@[@"Return", @"Accept"]
                            onCancel:^{  
            NSLog(@"Cancel button tapped");
        }onOther:^(NSInteger index) {
            if (index == 0) {
                NSLog(@"First other button tapped");
            }
            else{
                NSLog(@"Second other button tapped");
            }
        }];

You can see that when the UIAlertView is created, the logic for handling each button has already been defined.

Let’s start writing this extension. We will create a UIViewController category called Addons.

UIViewController+Addons.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <Foundation/Foundation.h>
typedef void(^HandleBlock)(void);
typedef void(^OtherHandleBlock)(NSInteger index);
@interface UIViewController(Addons)<UIAlertViewDelegate>
/**
 *  Show an alert view
 *
 *  @param title             title
 *  @param message           message
 *  @param cancelButtonTitle title for the cancel button
 *  @param otherButtonTitles array of titles for other buttons
 *  @param cancelBlock       block executed when the cancel button is tapped
 *  @param otherBlock        block executed when another button is tapped
 */
- (void)showAlertViewWithTitle:(NSString *) title
                      message:(NSString *) message
            cancelButtonTitle:(NSString *) cancelButtonTitle
            otherButtonTitles:(NSArray *) otherButtonTitles
                     onCancel:(CancelBlock) cancelBlock
                      onOther:(OtherHandleBlock) otherBlock;

Two block types are defined. The first is used when the cancel button is tapped, and the second is used when another button is tapped. Because there may be multiple other buttons, an index parameter is added to determine which button was tapped.

UIViewController+Addons.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#import "UIViewController+Addons.h"
#import <objc/runtime.h>
static char kVCActionHandleAlertCancelBlockKey;
static char kVCActionHandleAlertOtherBlockKey;
@implementation UIViewController(Addons)
-(void)showAlertViewWithTitle:(NSString *)title
                      message:(NSString *)message
            cancelButtonTitle:(NSString *)cancelButtonTitle
            otherButtonTitles:(NSArray *)otherButtonTitles
                     onCancel:(CancelBlock)cancelBlock
                      onOther:(OtherHandleBlock)otherBlock{
     UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:title
                                                       message:message
                                                      delegate:self
                      	                   cancelButtonTitle:cancelButtonTitle
                                             otherButtonTitles:nil];
    for(NSString *buttonTitle in otherButtonTitles){
        [alertView addButtonWithTitle:buttonTitle];
    }
    if (cancelBlock) {
        objc_setAssociatedObject(self, &kVCActionHandleAlertCancelBlockKey, cancelBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    if (otherBlock) {
        objc_setAssociatedObject(self, &kVCActionHandleAlertOtherBlockKey, otherBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    [alertView show];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
	if(buttonIndex == alertView.cancelButtonIndex){
        CancelBlock cancelBlock = objc_getAssociatedObject(self, &kVCActionHandleAlertCancelBlockKey);
        if(cancelBlock){
            cancelBlock();
        }
    }
    else{
        OtherHandleBlock otherBlock = objc_getAssociatedObject(self, &kVCActionHandleAlertOtherBlockKey);
        if(otherBlock){
            otherBlock(buttonIndex - 1);
        }
    }
}

Remember to import the <objc/runtime.h> header, create the UIAlertView, and then associate the two incoming blocks with the UIViewController object, which is this step:

1
2
3
4
5
6
7
 if (cancelBlock) {
        objc_setAssociatedObject(self, &kVCActionHandleAlertCancelBlockKey, cancelBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    if (otherBlock) {
        objc_setAssociatedObject(self, &kVCActionHandleAlertOtherBlockKey, otherBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }

Note that the association policy here should preferably use OBJC_ASSOCIATION_COPY_NONATOMIC so that ARC can manage the block. The delegate method later simply retrieves the block from self and executes it.

Of course, UIActionSheet and even UIImagePickerController can also be extended this way.

This post is licensed under CC BY 4.0 by the author.