15

In my application I use a UIAlertView to display to the user a message and some options. Depending on the button pressed, I want the application to perform something on an object. The sample code I use is...

-(void) showAlert: (id) ctx {
    UIAlertView *baseAlert = [[UIAlertView alloc] 
                          initWithTitle: title
                          message: msg
                          delegate:self
                          cancelButtonTitle: cancelButtonTitle
                          otherButtonTitles: buttonTitle1, buttonTitle2, nil];
    //baseAlert.context = ctx;
    [baseAlert show];
    [baseAlert release];
}


- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
    if (buttonIndex == 1) {
        id context = ...;//alertView.context;
        [self performSelectorOnMainThread:@selector(xxx:) withObject: context waitUntilDone: NO];
    }
}

Is there any way to pass an object into the delegate as a context object? or maybe some other way?

I could add the property on the delegate but the same delegate object is being used by many different alert views. For this reason I would prefer a solution where the context object is attached to the UIAlertView instance and carried across to the delegate as part of the UIAlertView object.

Panagiotis Korros
  • 10,840
  • 12
  • 41
  • 43

8 Answers8

12

I still think storing it locally is the best solution. Create a class local NSMutableDictionary variable to hold a map of context objects, store the context with UIAlertView as the key and the context as the value.

Then when the alert method is called just look into the dictionary to see which context object is related. If you don't want to use the whole Alert object as a key, you could use just the address of the UIAlertView object:

NSString *alertKey = [NSString stringWithFormat:@"%x", baseAlert];

The address should be constant on the phone. Or you could tag each alert as the other poster suggested and use the tag to look up a context in the map.

Don't forget to clear out the context object when you are done!

Kendall Helmstetter Gelner
  • 74,769
  • 26
  • 128
  • 150
  • "The address should be constant on the phone" <- such address is a pointer to an object, and as such can not be relied upon to stay constant. The rest of your answer is sensible, I just wanted to point out this for future reference, in case somebody else landed on this page. – Morpheu5 May 19 '14 at 20:53
10

A complete implementation that allows you to pass context:

@interface TDAlertView : UIAlertView
@property (nonatomic, strong) id context;
@end

@implementation TDAlertView
@end

And a usage example, note how we pre-cast the pointer:

@implementation SomeAlertViewDelegate
- (void)alertView:(TDAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
     NSLog(@"%@", [alertView context]);
}
@end
mxcl
  • 26,392
  • 12
  • 99
  • 98
  • 4
    I wonder why they don't have something like this built in. It seems like passing context to a delegate would be a pretty common problem – pepsi Jun 09 '11 at 17:02
  • As @Jeffery Thomas mentioned, subclassing UIAlertView is not a good idea. – Josh Brown Mar 04 '13 at 19:26
  • 1
    @JoshBrown my subclass is absolutely fine it doesn't modify anything about the class, which is what the documentation is warning against. Effectively the subclass is just syntactic sugar. – mxcl Mar 05 '13 at 03:16
  • 2
    I don't consider it safe to do things Apple explicitly says not to do. Consider a category instead. – Josh Brown Mar 06 '13 at 15:44
  • 1
    @JoshBrown …you can't add new properties to a category. But whatever, subclassing is not what Apple are warning about, (despite their language to that effect) overriding methods and changing the behavior is the problem. The larger warning is there to disuade programmers who don't understand how objc works from creating code that might (one day) break. I guess, whatever, a message to people reading my answer: if you don’t understand how this code compiles, don’t use my code, if you do, then you’ll already know it is safe and probably are already using this method or something better. – mxcl Mar 06 '13 at 17:45
  • I'm guessing you don't know about `objc_getAssociatedObject()` and `objc_setAssociatedObject()`. They allow additional data to be stored in an object. I've used them a number of times to add new properties to a category. I just added a new answer that works around the subclassing prohibition. – Jeffery Thomas Mar 08 '13 at 03:33
7

you can also use the tag property (since it's a UIView subclass). This is just an int, but may be enough for you.

Ben Gottlieb
  • 85,404
  • 22
  • 176
  • 172
3

Subclassing UIAlertView is not a good idea.

http://developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/UIAlertView_Class/UIAlertView/UIAlertView.html

Subclassing Notes

The UIAlertView class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified.

UIAlertView with a user supplied context and [self autorelease] answers this question in a different way.

Community
  • 1
  • 1
Jeffery Thomas
  • 42,202
  • 8
  • 92
  • 117
  • Precisely as the documentation states: it's fine to subclass as long as you don’t override anything. – mxcl Mar 05 '13 at 03:15
  • @MaxHowell The line in the documentation is "does not support subclassing". It can't implicitly allow something it explicitly prohibits. The prohibition on modifying the view hierarchy is in addition to the prohibition on subclassing. – Jeffery Thomas Mar 05 '13 at 04:39
  • Well I guess, interpretation is interpretation. Based on my experience and knowledge about how objc works under the hood, and how Apple document their classes, I'm 100% certain you can subclass UIAlertView in the manner I do in my answer. – mxcl Mar 05 '13 at 19:22
  • Two wrongs don't make a right. While your other answer is fine, this one is not. You should not use SO answers to try to wrap up the discussion from comments in a different answer nor for requesting help towards a different question of your own. – Ricardo Sanchez-Saez Apr 03 '13 at 14:14
  • @rsanchezsaez I'm sorry if my answer doesn't meet your expectations, however I feel my link was directly relevant to this question. The link provided an answer for this exact question, but I edited the final line to remove the request for help. – Jeffery Thomas Apr 03 '13 at 20:51
  • I understand. In any case, let me insist in that there's no point for having two different answers by the same user for the same question. You should have edited whichever answer you posted first to reflect all the information in just one place. – Ricardo Sanchez-Saez Apr 04 '13 at 08:09
  • @rsanchezsaez They are two entirely different technical solutions to the same problem. SO allows a user to post multiple answers for this exact reason. See: http://meta.stackexchange.com/questions/94183/when-should-one-add-another-answer-instead-of-editing-ones-already-posted – Jeffery Thomas Apr 04 '13 at 12:42
3

Instead of debating the meaning of "does not support subclassing", I'll provide a better answer. I created a generic contextInfo category for my job a couple months ago. I just put it on github: JLTContextInfo.

#import "objc/runtime.h"

@interface NSObject (JLTContextInfo)
- (NSMutableDictionary *)JLT_contextInfo;
@end
@implementation NSObject (JLTContextInfo)
- (NSMutableDictionary *)JLT_contextInfo
{
    static char key;
    NSMutableDictionary *contextInfo = objc_getAssociatedObject(self, &key);
    if (!contextInfo) {
        contextInfo = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, &key, contextInfo, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return contextInfo;
}
@end

This creates a place to easily store extra data for any object derived from NSObject. Now the answer is nearly identical to the original question.

-(void) showAlert: (id) ctx {
    UIAlertView *baseAlert = [[UIAlertView alloc] 
                          initWithTitle: title
                          message: msg
                          delegate:self
                          cancelButtonTitle: cancelButtonTitle
                          otherButtonTitles: buttonTitle1, buttonTitle2, nil];
    [[baseAlert JLT_contextInfo] setObject:ctx forKey:@"ctx"];
    [baseAlert show];
    [baseAlert release];
}

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
    if (buttonIndex == 1) {
        id context = [[alertView JLT_contextInfo] objectForKey:@"ctx"];
        [self performSelectorOnMainThread:@selector(xxx:) withObject: context waitUntilDone: NO];
    }
}
Jeffery Thomas
  • 42,202
  • 8
  • 92
  • 117
1

I did a mix between Kendall's answer and the uses of blocks in one of my base view controller classes. Now I can use AlertView and ActionSheets with blocks which improves greatly readability. Here is how I did it :

In the .h of my ViewController I declare a block type (optional but recommanded)

typedef void (^AlertViewBlock)(UIAlertView*,NSInteger);

Also I declare a mutable dictionnary that will store the blocks for each alertview :

NSMutableDictionary* m_AlertViewContext;

In the implementation file I add a method to create the AlertView and save the block :

-(void)displayAlertViewWithTitle:(NSString *)title message:(NSString *)message cancelButtonTitle:(NSString *)cancelButtonTitle withBlock:(AlertViewBlock)execBlock otherButtonTitles:(NSArray *)otherButtonTitles
    {
        UIAlertView* alert = [[UIAlertView alloc] initWithTitle:title
                                                        message:message
                                                       delegate:self cancelButtonTitle:cancelButtonTitle otherButtonTitles: nil];
        for (NSString* otherButtonTitle in otherButtonTitles) {
            [alert addButtonWithTitle:otherButtonTitle];
        }
        AlertViewBlock blockCopy = Block_copy(execBlock);
        [m_AlertViewContext setObject:blockCopy forKey:[NSString stringWithFormat:@"%p",alert]];
        Block_release(blockCopy);
        [alert show];
        [alert release];
    }

Note that I receive the same attributes as the constructor of the UIAlertView but the delegate (which will be self). Also I receive a AlertViewBlock object that I save in the m_AlertViewContext mutable dictionnary. Then I show the alert as I would usually do.

In the delegate callbacks, I call the block and give it the parameters :

#pragma mark -
#pragma mark UIAlertViewDelegate

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    NSString* blockKey = [NSString stringWithFormat:@"%p",alertView];
    AlertViewBlock block  = [m_AlertViewContext objectForKey:blockKey];
    block(alertView,buttonIndex);
    [m_AlertViewContext removeObjectForKey:blockKey];
}

- (void)alertViewCancel:(UIAlertView *)alertView {
    NSString* blockKey = [NSString stringWithFormat:@"%p",alertView];
    [m_AlertViewContext removeObjectForKey:blockKey];
}

Now, whenever I need to use an AlertView I can call it like this :

[self displayAlertViewWithTitle:@"Title"
                                message:@"msg"
                      cancelButtonTitle:@"Cancel"
         withBlock:^(UIAlertView *alertView, NSInteger buttonIndex) {
             if ([[alertView buttonTitleAtIndex:buttonIndex] isEqualToString:@"DO ACTION"]){
                 [self doWhatYouHaveToDo];
             }
         } otherButtonTitles:[NSArray arrayWithObject:@"DO ACTION"]];

I did the same for the ActionSheet and now it's really easy to use those. Hope it helps.

mbritto
  • 898
  • 12
  • 25
1

From my other answer, here's a quick and clean solution that takes advantage of associated objects. I mention in my other answer that you could even replace UIAlertView with NSObject and effectively add a context property to any object:

#import <objc/runtime.h>

@interface UIAlertView (Private)
@property (nonatomic, strong) id context;
@end

@implementation UIAlertView (Private)
@dynamic context;
-(void)setContext:(id)context {
    objc_setAssociatedObject(self, @selector(context), context, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(id)context {
    return objc_getAssociatedObject(self, @selector(context));
}
@end

And then you'll be able to do something like:

NSObject *myObject = [NSObject new];

UIAlertView *alertView = ...
alertView.context = myObject;

IMPORTANT: And don't forget to nil the context in dealloc!!

Community
  • 1
  • 1
lobianco
  • 6,226
  • 2
  • 33
  • 48
0

You could subclass UIAlertView and add the property there.

diederikh
  • 25,221
  • 5
  • 36
  • 49