displaying a dialable phone number withing the text for a UIAlertView - iphone

is there a way that part of the text that is being displayed in an iphone UIAlertView will be a phone number that when clicked will be dialed?
maybe using tel: somehow ?

If you like to implement the dataDetectorType in your message text there is no native way to do it.
The only way is to subclass the UIAlertView and customize the init method like this :
- (id)initWithTitle:(NSString *)title message:(NSString *)message delegate:(id)delegate cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles, ... {
self = [super initWithTitle:title message:nil delegate:delegate cancelButtonTitle:cancelButtonTitle otherButtonTitles:otherButtonTitles, nil];
if (self) {
CGRect alertFrame = [self frame];
UITextView myTextView = [[UITextView alloc] initWithFrame:CGRectMake(alertFrame.origin.x + 10, alertFrame.origin.y + 44, 200, 44)];
[myTextView setEditable:NO];
[myTextView setBackgroundColor:[UIColor clearColor]];
[myTextView setDataDetectorTypes:UIDataDetectorTypeAll];
[myTextView setText:#"http://www.apple.com"]; // Use your original message string from init
[self addSubview:myTextView];
[myTextView release]
}
return self;
}
I tested it right now and it works but you need to spend a little bit to make it presentable :P
Maybe using the way posted by Jhaliya is quickly and more clean.

Yes, This is possible by setting the delegate ( UIAlertViewDelegate ) of your UIAlertView. Read the message and title from your UIAlertView by using below properties.
#property(nonatomic, copy) NSString *message
#property(nonatomic, copy) NSString *title
You need to implement the delegte method in order to get the info for which button pressed.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
you could also follow link for getting pressed button index and dial an Phone number by Using tel: .
So, your code could be something like below .
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
//Access `title` and `message` of your **UIAlertView**
NSString* alertTitle = alertView.title;
NSString* alertMessage = alertView.message;
// Formatted the phone number and assign it to a string.
NSString* myFormattedPhNumber = /*Use StringWithFormat function of NSString */;
if (buttonIndex == 0)
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:myFormattedPhNumber];
}
}

Related

UIPickerview Multiple TextFields Xcode

I'm looking for a way to use a single UIPickerview for two different textfields. I'd like to have the pickerview popup when each textfield is selected. After the user selects their item, the item will populate the specific text field. The picker would have to populate based on the textfield chosen.
I've read this:
How to use one UIPickerView for multiple textfields in one view?
and this:
How to use UIPickerView to populate different textfields in one view?
and this:
Multiple sources for UIPickerView on textfield editing
However, none gives a complete solution.
I'm very new at Xcode so I'd like a solution that includes steps to set the storyboard also.
I appreciate any help as i've researched this for weeks.
EDIT: HERE IS MY CODE:
.h:
#import <UIKit/UIKit.h>
#interface klViewController : UIViewController <UIPickerViewDelegate, UIPickerViewDataSource> {
IBOutlet UITextField *textField1;
IBOutlet UITextField *textField2;
NSMutableArray *pickerArray1;
NSMutableArray *pickerArray2;
UIPickerView *pickerView;
}
#property(nonatomic,retain) IBOutlet UITextField *textField1;
#property(nonatomic,retain) IBOutlet UITextField *textField2;
#property(nonatomic,retain) IBOutlet UIPickerView *pickerView;
#end
.m:
#import "klViewController.h"
#interface klViewController ()
#end
#implementation klViewController
#synthesize pickerView;
#synthesize textField1;
#synthesize textField2;
int variabla;
-(void)textFieldDidBeginEditing:(UITextField *)textField{
[pickerView setHidden:YES];
if (textField1.editing == YES) {
[textField1 resignFirstResponder];
[pickerView setHidden:NO];
variabla = 1;
}else if (textField2.editing == YES) {
[textField2 resignFirstResponder];
[pickerView setHidden:NO];
variabla = 2;
}
NSLog(#"variabla %d",variabla);
[pickerView reloadAllComponents];
}
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView;
{
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component;
{
if (variabla == 1) {
return [pickerArray1 count];
}else if (variabla == 2) {
return [pickerArray2 count];
}else {
return 0;
}
}
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component;
{
if (variabla == 1) {
return [pickerArray1 objectAtIndex:row];
}else if (variabla == 2) {
return [pickerArray2 objectAtIndex:row];
}else {
return 0;
}
}
- (void)textFieldShouldReturn:(UITextField *)textField{
[textField resignFirstResponder];
}
- (void)viewDidLoad {
[super viewDidLoad];
[pickerView setHidden:YES];
pickerArray1 = [[NSMutableArray alloc] initWithObjects:#"0", #"1", #"2", nil];
pickerArray2 = [[NSMutableArray alloc] initWithObjects:#"3", #"4", #"5", nil];
}
#end
HERE IS A SCREEN SHOT OF MY STORYBOARD:
STATUS UPDATE:
When I run the program:
1) the pickerview is hidden.
2) when I select a textfield, the picker view appears and populates correctly depending on the textfield selected.
PROBLEMS:
1) Picker doesn't go away when click outside of the textfield.
2) Textfields don't populated when a row in the Picker is selected.
Hope this provides more insight.
1) Picker doesn't go away when click outside of the textfield.
You have no code that attempts to make the picker go away when that happens. Try adding a simple tap gesture recognizer to the view. Add a line to viewDidLoad like:
[self.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(backgroundTap:)]];
Then implement the function simply like:
-(void)backgroundTap:(UITapGestureRecognizer *)tapGR{
self.pickerView.hidden = YES;
// And maybe..
variabla = 0;
}
Since you are making the picker appear and disappear using the hidden property this will work very simply. There are other more sophisticated ways to do this which I hope you explore. Generally the picker is set as the textfield's inputView property; that is worth investigating.
2) Textfields don't populated when a row in the Picker is selected.
You haven't handled the picker's pickerView:didSelectRow:inComponent: delegate method. That's the method that gets called when the picker stops turning and lands on an item. Don't assume that this is the item the user selected it will be called multiple times.
-(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component{
NSString *text = [self pickerView:pickerView titleForRow:row forComponent:component];
UITextField *current = nil;
if (variabla == 1) current = self.textField1;
else if (variabla == 2) current = self.textField2;
current.text = text;
}
That should get your implementation working. One more thing though variabla is an instance variable and should be declared in curly braces immediately following the #interface or #implementation line.
#implementation klViewController {
int variabla;
}
#synt....
Give the Tag of two Different textfield for identify which textfield you select and everything is ok you just need to change below method.
Hope This Work
-(void)textFieldDidBeginEditing:(UITextField *)textField {
if ([textField viewWithTag:100]) {
[textField resignFirstResponder];
[self.View1 setHidden:NO];
variable=1;
}
else if ([textField viewWithTag:101]) {
[textField resignFirstResponder];
[self.View1 setHidden:NO];
variable=2;
}
[_Picker_view reloadAllComponents];
}
Thanks :)
Make sure you have declared the Picker View object in the header file.
In the header file, import the UITextFieldDelegate protocol:
#interface MyView:UIViewController < UITextFieldDelegate>
In IB, set a tag for each text field you have.
In the *.m file, implement the textFieldShouldBeginEditing method, update the PickerView array Data Source and Reload all the picker view components.
-(BOOL) textFieldShouldBeginEditing:(UITextField *)textField {
if (textField.tag == 1) {
itemsArray = [[NSArray alloc] arrayWithObjects:#"A", #"B"];
}
if (textField.tag == 2) {
itemsArray = [[NSArray alloc] arrayWithObjects:#"Green", #"Yellow"];
}
[myPickerView reloadAllComponents];
}
Make sure you import the UIPickerViewDelegate and UIPickerViewDataSource in the header file.
You can use the same picker view for as many text fields as you want, to change the content of the picker view according to the selected text field you need to replace the data source of the picker view with different items whenever a text field is being selected and then reload the picker view components.
Well my friend, I'm afraid it's a little bit to complicated to explain here.
You can set object's tag value in the IB properties menu.
Once you dragged a PickerView into your view and it's selected, you can change the tag attribute in the Object Properties menu (on the side).
I think you should look for tutorials that will show you how to setup a simple picker view, or table view (they work very similar to one another) and that will give you much more information on how to do what you want to do. In a nutshell, every Picker View takes information from a Data Source, you can create an array that contains some strings and have the picker view load each item in the array as a row. I have a small website with some beginners information, check it out.
http://i-tutor.weebly.com/index.html
In .h
#interface TQViewController : UIViewController<UIPickerViewDelegate>
{
UITextField *textfield;
UIPickerView *Picker1;
NSArray *Array1,*Array2;
}
end
and in .m
- (void)viewDidLoad
{
[super viewDidLoad];
//TextField
textfield=[[UITextField alloc]initWithFrame:CGRectMake(5,5,310,40)];
textfield.borderStyle = UITextBorderStyleRoundedRect;
textfield.backgroundColor=[UIColor whiteColor];
textfield.textAlignment = UITextAlignmentCenter;
textfield.placeholder = #"<enter amount>";
[self.view addSubview:textfield];
textfield1=[[UITextField alloc]initWithFrame:CGRectMake(5,100,310,40)];
textfield1.borderStyle = UITextBorderStyleRoundedRect;
textfield1.backgroundColor=[UIColor whiteColor];
textfield1.textAlignment = UITextAlignmentCenter;
textfield1.placeholder = #"<enter amount>";
[self.view addSubview:textfield1];
// PickerView1
Array1=[[NSArray alloc]initWithObjects:#"USD",#"INR",#"EUR", nil];
Picker1=[[UIPickerView alloc]initWithFrame:CGRectMake(0, 50, 320,10)];
Picker1.delegate=self;
Picker1.tag=PICKER1_TAG;
Picker1.showsSelectionIndicator=YES;
[self.view addSubview:Picker1];
Array2=[[NSArray alloc]initWithObjects:#"USD",#"INR",#"EUR", nil];
}
-(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
if([textfield becomeFirstResponder])
return [Array1 count];
if([textfield1 becomeFirstResponder])
return [Array2 count];
}
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
NSString *title;
if([textfield becomeFirstResponder])
{
title=[Array1 objectAtIndex:row];
return title;
}
([textfield1 becomeFirstResponder])
{
title=[Array2 objectAtIndex:row];
return title;
}
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
if([textfield becomeFirstResponder])
{
//your code
}
([textfield1 becomeFirstResponder])
{
//your code
}
}

How do I reloadData in a tableView when a UIAlertView button has been pressed?

I have a list of names in a UITableView. The tableView's delegate is a viewController. I use an IBAction to call an UIAlertView (with a text field) so the user can add another name to the tableView:
- (IBAction)addGuest:(UIButton *)sender
{
// open a alert with text field, cancel and add button
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Add New Guest" message:#"Example: John Doe" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Add", nil];
alert.alertViewStyle = UIAlertViewStylePlainTextInput;
[alert show];
}
When the user clicks the 'Add' button in the UIAlertView, I write the information in the text field to a plist:
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *plistPath = [documentsPath stringByAppendingPathComponent:#"Guests.plist"];
[self.guestList addObject:[[alertView textFieldAtIndex:0] text]];
NSMutableDictionary *plistDict = [NSMutableDictionary dictionaryWithObjects:[NSArray arrayWithObjects:guestList, nil] forKeys:[NSArray arrayWithObjects:#"nameList", nil]];
NSString *error = nil;
NSData *plistData = [NSPropertyListSerialization dataFromPropertyList:plistDict format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];
if (plistData) {
[plistData writeToFile:plistPath atomically:YES];
} else {
NSLog(#"Error in saveData: %#", error);
}
}
}
At this point, I'd like to do a reload on the tableView (inside the above method) so the newly added name appears. I've set the UIAlertView's delegate to self.
However, [self.tableView reloadData] doesn't work because tableView isn't recognized in this method. I've tried lots of variations, but I can't get anything to work.
How do I do this ?
Thank you!
Every view controller has a view. Is the view of your table view's delegate controller the table view? If so, try:
[((UITableView *) self.view) reloadData]
in the implementation of the didDismissWithButtonIndex method. The above only works if the tableView's delegate controller is also the delegate for the UIAlertView. You would accomplish that with: `
#interface SomeViewController : UIViewController
<UITableViewDataSource,
UITableViewDelegate,
UIAlertViewDelegate> { /* .... */ } /* ... */ #end
or better (but not always possible):
#interface SomeViewController : UITableViewController // <- Now you'll have self.tableView
< UIAlertViewDelegate> { /* .... */ } /* ... */ #end
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
NSString *buttonTitle=[alertView buttonTitleAtIndex:buttonIndex];
if ([buttonTitle isEqualToString:#"Add"]) {
[tableviewName reloadData];
}
}
Create an outlet for your table view, connect it in inteface builder
in .h
IBOutlet UITableView *exercisesTable;
in .m class
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0)
{
}
else if (buttonIndex == 1)
{
// your code
[exercisesTable reloadData];
}
}
In order to use [tableview reloadData] method you should implement the
uitableviewdelegate,uitabbleviewdatasource
reloadData works only if you implement the following three delegate methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Instead of using - (IBAction)addGuest:(UIButton *)sender you can use the delegate method
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
NB:dont forget to set the delegate and datasource to tableview to the viewcontroller

resignFirstResponder UITextView

I am using a UITEXTVIEW. I am trying to send a resignFirstResponder when the done button is pressed. I am not sure how to trigger the method containing the resignFirstResponder line.
I have set the UITextView's delegate to the File's Owner in Interface Builder. This is my viewcontroller's header and implementation file:
#import <Foundation/Foundation.h>
#import "Question.h"
#interface CommentQuestionViewController : UIViewController {
Question *question;
IBOutlet UILabel *questionTitle;
IBOutlet UITextView *inputAnswer; //this is the textview
}
#property (nonatomic, retain) UILabel *questionTitle;
#property (nonatomic, retain) Question *question;
#property (nonatomic, retain) UITextView *inputAnswer;
- (void) addButton:(id)sender isLast:(BOOL)last;
- (void) setQuestionId:(NSString*)quId withTitle:(NSString*)quTitle number:(NSString*)quNum section:(NSString*)sId questionType:(NSString*)qt;
#end
#import "CommentQuestionViewController.h"
#implementation CommentQuestionViewController
#synthesize questionTitle, question, inputAnswer;
- (void) addButton:(id)delegate isLast:(BOOL)last{
UIBarButtonItem *anotherButton;
anotherButton = [[UIBarButtonItem alloc] initWithTitle:#"Done" style:UIBarButtonItemStylePlain target:delegate action:#selector(finishQuestionnaire:)];
self.navigationItem.rightBarButtonItem = anotherButton;
[anotherButton release];
}
-(void)viewDidLoad{
//self.questionTitle.text = question.qTitle;
[[self questionTitle] setText:[question qTitle]];
[super viewDidLoad];
NSString *str = [NSString stringWithFormat:#"Question %#", [question qNumber]];
//self.title = str;
[self setTitle:str];
[str release];
}
-(void) setQuestionId:(NSString*)quId withTitle:(NSString*)quTitle number:(NSString*)quNum section:(NSString*)sId questionType:(NSString*)qt{
question = [[Question alloc]init];
[question setQId:quId];
[question setQTitle:quTitle];
[question setQNumber:quNum];
[question setSectionId:sId];
[question setQType:qt];
}
- (void) viewWillAppear:(BOOL)animated{
self.navigationItem.hidesBackButton = YES;
}
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
- (void) textViewDidEndEditing:(UITextView*)textView{
[textView resignFirstResponder];
}
- (void)viewDidUnload {
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (void)dealloc {
[inputAnswer release];
[questionTitle release];
[question release];
[super dealloc];
}
You can try this:
–(BOOL)textView:(UITextView*)textView shouldChangeCharactersInRange:(NSRange)range replacementText:(NSString*)text {
if ([text isEqualToString:#"\n"]) {
[textView resignFirstResponder];
return NO;
}
else
return YES;
}
Of course, you won't be able to type actual carriage returns if you do this.
After looking at your source code, the link that I had posted is pointless. I thought you were talking about the done key on the keyboard. But now I see that you were talking about an instance UIButton on the navigation bar, right?
Your addButton:isLast: method is looking great so far, but I'd change it a bit so it adheres to the Apple HIG:
- (void)addButton:(id)delegate isLast:(BOOL)last
{
UIBarButtonItem *anotherButton;
anotherButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:delegate
action:#selector(finishQuestionnaire:)];
self.navigationItem.rightBarButtonItem = anotherButton;
[anotherButton release];
}
It's pretty much the same, except this one creates a button with the appropriate style.
Anyway, I don't see how the button is being added since addButton:isLast: is not being called in viewDidLoad:. Add the following line in your viewDidLoad:.
[self addButton:self isLast:NO];
last is redundant in your addButton:isLast: since it's not even being use so I just pass whatever (NO).
You're almost done, but if you press that button, your application would crash since finishQuestionnaire: is not implemented on self (CommentQuestionViewController). And since you want to dismiss the keyboard when the user presses your done button, that's where we will also resign your text view as first responder. Just add the following method:
- (void)finishQuestionnaire:(id)sender
{
[inputAnswer resignFirstResponder];
}
It's BOOL that's it..
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if([text isEqualToString:#"\n"]) {
[textView resignFirstResponder];
return NO;
}
return YES;
}
If that doesn't work please check the following things..
Whether your textView is connected to the textView property defined in your class.. (You can see this in your xib, with an arrow icon).
2.Check whether the textView delegate is written in your .h file.
#interface YourViewController : UIViewController <UITextViewDelegate>
3.Check whether the textView's delegate is assigned.
//set this in the viewDidLoad method
self.textView.delegate = self;
Check for new line character by this method.
-(NSRange)rangeOfCharacterFromSet:(NSCharacterSet *)aSet
Then check the length of the text, if it is one character length, then new line character is entered, not pasted.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
BOOL hasNewLineCharacterTyped = (text.length == 1) && [text rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet]].location != NSNotFound;
if (hasNewLineCharacterTyped) {
[textView resignFirstResponder];
return NO;
}
return YES;
}

UIAlertView Delegates

Can someone explain how the delegate to a UIAlertView works? Is it automatically called or do I have to call it? Eg:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
Let's say you showed an alert where the delegate was "self"
- (void)showAlert {
UIAlertView *myAlert = [[UIAlertView alloc] initWithTitle:#"My Alert"
message:#"Do you want to continue?"
delegate:self
cancelButtonTitle:nil
otherButtonTitles:#"No", #"Yes", nil];
[myAlert show];
[myAlert release];
}
In order for the following to work in your .m file:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
Your .h file will need to reference the UIAlertViewDelegate in the implementation statement like so:
#interface myViewController : UIViewController <UIAlertViewDelegate> {
}
This is what allows your .m file to respond to UIAlertViewDelegate method calls.
So long as you're correctly setting the delegate property of the UIAlertView and implementing the protocol, it will be automatically called when a user clicks on a button in your alert.
Take a look at the projects listed under "Related sample code" at http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIAlertViewDelegate_Protocol/UIAlertViewDelegate/UIAlertViewDelegate.html to see it in action.
Here is a wrapper for the delegate so that you can use blocks instead. The flow of execution will be the same but the flow of the code will be easier to follow. So, usage:
[YUYesNoListener yesNoWithTitle:#"My Title" message:#"My Message" yesBlock:^
{
NSLog(#"YES PRESSED!");
}
noBlock:^
{
NSLog(#"NO PRESSED!");
}];
...and here is the helper class:
typedef void(^EmptyBlockType)();
#interface YUYesNoListener : NSObject <UIAlertViewDelegate>
#property (nonatomic, retain) EmptyBlockType yesBlock;
#property (nonatomic, retain) EmptyBlockType noBlock;
+ (void) yesNoWithTitle:(NSString*)title message:(NSString*)message yesBlock:(EmptyBlockType)yesBlock noBlock:(EmptyBlockType)noBlock;
#end
#implementation YUYesNoListener
#synthesize yesBlock = _yesBlock;
#synthesize noBlock = _noBlock;
- (id) initWithYesBlock:(EmptyBlockType)yesBlock noBlock:(EmptyBlockType)noBlock
{
self = [super init];
if (self)
{
self.yesBlock = [[yesBlock copy] autorelease];
self.noBlock = [[noBlock copy] autorelease];
}
return self;
}
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 0 && self.noBlock)
self.noBlock();
else if (buttonIndex == 1 && self.yesBlock)
self.yesBlock();
[_yesBlock release];
[_noBlock release];
[alertView release];
[self release];
}
- (void) alertViewCancel:(UIAlertView *)alertView
{
if (self.noBlock)
self.noBlock();
[_yesBlock release];
[_noBlock release];
[alertView release];
[self release];
}
+ (void) yesNoWithTitle:(NSString*)title message:(NSString*)message yesBlock:(EmptyBlockType)yesBlock noBlock:(EmptyBlockType)noBlock
{
YUYesNoListener* yesNoListener = [[YUYesNoListener alloc] initWithYesBlock:yesBlock noBlock:noBlock];
[[[UIAlertView alloc] initWithTitle:title message:message delegate:yesNoListener cancelButtonTitle:#"No" otherButtonTitles:#"Yes", nil] show];
}
#end
The alertView:clickedButtonAtIndex: method of the delegate is automatically called by UIAlertView. The init method for UIAlertView takes a delegate as one of the parameters. Just make sure to pass in an object that responds to alertView:clickedButtonAtIndex:.

How to safely pass a context object in an UIAlertView delegate?

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.
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!
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
you can also use the tag property (since it's a UIView subclass). This is just an int, but may be enough for you.
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];
}
}
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.
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.
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!!
You could subclass UIAlertView and add the property there.