Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best Technique for Replacing Delegate Methods with Blocks

I'm looking to create a category to replace delegate methods with callbacks blocks for a lot of the simple iOS APIs. Similar to the sendAsyc block on NSURLConnection. There are 2 techniques that are leak free and seem to work fine. What are the pros/cons about each? Is there a better way?

Option 1. Use a category to implement the delegate's callback method on NSObject with the external callback block scoped.

// Add category on NSObject to respond to the delegate
@interface NSObject(BlocksDelegate)
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex;
@end

@implementation NSObject(BlocksDelegate)
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    // Self is scoped to the block that was copied
    void(^callback)(NSInteger) = (id)self;
    // Call the callback passed if
    callback(buttonIndex);
    [self release];
}
@end

// Alert View Category
@implementation UIAlertView (BlocksDelegate)
+ (id) alertWithTitle:(NSString*)title
              message:(NSString*)message
         clickedBlock:(void(^)(NSInteger))buttonIndexClickedBlock
    cancelButtonTitle:(NSString*)cancelButtonTitle
    otherButtonTitles:(NSString*)otherButtonTitles
{
    // Copy block passed in to the Heap and will stay alive with the UIAlertView
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title 
                                                    message:message 
                                                   delegate:[buttonIndexClickedBlock copy]
                                          cancelButtonTitle:cancelButtonTitle 
                                          otherButtonTitles:otherButtonTitles, nil];

    // Display the alert
    [alert show];

    // Autorelease the alert
    return [alert autorelease];
}

@end

This adds a lot of methods on the NSObject and seems like it could cause issues with any other class trying to use the standard delegate method. But it keeps the block alive with the object and returns the callback without any leaks that I've found.


Option 2. Create an light-weight class to contain the block, dynamicly associate it with the class so it will stay in the heap and remove it when the callback is complete.

// Generic Block Delegate
@interface __DelegateBlock:NSObject
typedef void (^HeapBlock)(NSInteger);
@property (nonatomic, copy) HeapBlock callbackBlock; 
@end

@implementation __DelegateBlock
@synthesize callbackBlock;
- (id) initWithBlock:(void(^)(NSInteger))callback
{
    // Init and copy Callback Block to the heap (@see accessor)
    if (self = [super init]) 
        [self setCallbackBlock:callback];
    return [self autorelease];
}
- (void) dealloc
{
    // Release the block
    [callbackBlock release], callbackBlock = nil;    
    [super dealloc];
}
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    // Return the result to the callback
    callbackBlock(buttonIndex);

    // Detach the block delegate, will decrement retain count
    SEL key = @selector(alertWithTitle:message:clickedBlock:cancelButtonTitle:otherButtonTitles:);
    objc_setAssociatedObject(alertView, key, nil, OBJC_ASSOCIATION_RETAIN);
    key = nil;

    // Release the Alert
    [alertView release];
}
@end

@implementation UIAlertView (BlocksDelegate)
+ (id) alertWithTitle:(NSString*)title
              message:(NSString*)message
         clickedBlock:(void(^)(NSInteger))buttonIndexClickedBlock
    cancelButtonTitle:(NSString*)cancelButtonTitle
    otherButtonTitles:(NSString*)otherButtonTitles
{
    // Create class to hold delegatee and copy block to heap
    DelegateBlock *delegatee = [[__DelegateBlock alloc] initWithBlock:buttonIndexClickedBlock];
    [[delegatee retain] autorelease];
    // Create delegater
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title 
                                                    message:message 
                                                   delegate:delegatee
                                          cancelButtonTitle:cancelButtonTitle 
                                          otherButtonTitles:otherButtonTitles, nil];

    // Attach the Delegate Block class to the Alert View, increase the retain count
    objc_setAssociatedObject(alert, _cmd, delegatee, OBJC_ASSOCIATION_RETAIN);

    // Display the alert
    [alert show];
    return alert;
}

@end

I like that this doesn't add anything on top of NSObject and things are a little more separated. It's attaching to the instance via the address of the function.

like image 236
puppybits Avatar asked Dec 27 '11 15:12

puppybits


1 Answers

I had a similar problem and chose your option 2, but with the 2 small additions:

  1. Explicitly marking the delegate it implements like this:

    @interface __DelegateBlock:NSObject <BlocksDelegate>
    
  2. Check to ensure the callback is not nil before calling:

    if (callbackBlock != nil) {
        callbackBlock(buttonIndex);
    }
    
like image 136
caleb Avatar answered Sep 21 '22 06:09

caleb