Using UIAlertView in a manner similar to Windows' MessageBox()? - iphone

I'm new to the iPhone and I would like to be able to use UIAlertView in a manner similar to the Windows MessageBox() or the MessageDlg() in Delphi.
For example, I have a method that needs to ask the user for confirmation on something, and proceed based on their response.
E.g. (pseudocode):
-(void)doSomething
{
[doStep1];
[doStep2];
var myValue = [getDefaultValue];
if (myValue = nil)
{
if [promptUser(#"No value in setting. Use the default value?")] //UIAlertView here?
{
myValue = #"defaultValue";
}
else
return; // bug out of the routine 'cause we have no value.
}
[doStep3 withValue:myValue];
}
Or, put put it another way- is there a way of using UIAlertView to ask the user a question within a routine as a way of controlling the logic flow of that routine?

There's no reason to subclass UIAlertView at all. That's what delegates are for. All you need is a class (such as your view controller) supporting the UIAlertViewDelegate protocol, and set the UIAlertView's delegate property to that class. You then implement the alertView:clickedButtonAtIndex: method in this class, and the alertViewCancel: method if you want to specifically handle cancellations differently.
You can read more about it in the UIAlertView documentation and in the UIAlertViewDelegate documentation.

I have no idea what MessageDlg() is, but you can certainly subclass UIAlertView and handle the dialog response, based on which button is pressed, e.g.:
Set up the UIAlertView subclass header:
//
// ARReachabilityAlertView.h
//
#import <UIKit/UIKit.h>
#interface ARReachabilityAlertView : UIAlertView <UIAlertViewDelegate> {
}
#end
Set up the UIAlertView subclass implementation:
//
// ARReachabilityAlertView.m
//
#import "ARReachabilityAlertView.h"
#implementation ARReachabilityAlertView
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setTitle:#"Error"];
[self setMessage:#"This application won't run without a network connection. Do you want to quit?"];
[self addButtonWithTitle:#"Quit"];
[self addButtonWithTitle:#"Continue"];
[self setDelegate:self];
}
return self;
}
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0)
exit(0); // quit application if "Quit" is pressed; otherwise, do nothing
}
- (void) drawRect:(CGRect)rect {
[super drawRect:rect];
}
- (void) dealloc {
[super dealloc];
}
#end
Note the alertView:clickedButtonAtIndex: delegate method. This handles the conditionals you use to decide how the application proceeds. You can send an NSNotification from here, or call a method in the application delegate, whatever you want.
In my example, this UIAlertView is instantiated if there is no network connection, and the application is closed if the user clicks "Quit" in the alert view. Otherwise, if the user clicks "Continue" the application keeps running as usual.
Note that the implementation of the subclass requires the drawRect: method be called. I'm not sure if this is a bug or not, since I'd expect the drawRect: method to be called in the super-class; I filed a bug report with Apple on this one, but I haven't heard anything. Comment it out if you want to see what the effect will be — it's kind of interesting.

I believe you're looking to use UIAlertView as a modal alert box (in the sense that you'd like your code to stop running until the user makes a selection). There's no easy way to do this, and it's really recommended that you NOT code for the iPhone in this manner. I think Alex's explanation is a good solution.

To do this, what you can do is to run the mainloop manually. I have not managed to stop the mainloop directly, so I instead run the mainloop for 0.5 seconds and wait until the user responds.
I ran into this question while researching the problem for a C#/MonoTouch user on the iPhone. The sample below is written for MonoTouch/C# but should be trivial to translate to Objective-C
The following function shows how you could implement a modal query with the above approach:
int WaitForClick ()
{
int clicked = -1;
var x = new UIAlertView ("Title", "Message", null, "Cancel", "OK", "Perhaps");
x.Show ();
bool done = false;
x.Clicked += (sender, buttonArgs) => {
Console.WriteLine ("User clicked on {0}", buttonArgs.ButtonIndex);
clicked = buttonArgs.ButtonIndex;
};
while (clicked == -1){
NSRunLoop.Current.RunUntil (NSDate.FromTimeIntervalSinceNow (0.5));
Console.WriteLine ("Waiting for another 0.5 seconds");
}
Console.WriteLine ("The user clicked {0}", clicked);
return clicked;
}

Just a quick tip: if you want a class to be a delegate for more than one UIAlertView, just use tag property to tell who is who:
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Clear all" message:#"Are you sure you want to erase everything?" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"OK", nil];
alert.tag = ALERT_DELETE_TAG;
[alert autorelease];
[alert show];
Delegate methods are called with UIAlertView as first argument, and you can check who's the originator there.

Thanks to #miguel.de.icaza I found my solution like below (part of the code):
#interface MyClass: NSObject <UIAlertViewDelegate>
{
int confirmed;
}
- (BOOL) removeObjectAtIndex:(NSUInteger)index;
#end
#implementation
- (BOOL) removeObjectAtIndex:(NSUInteger)index
{
// delete confirmation alert
confirmed = -1;
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Delete confirmation" message:#"Are you sure to delete object" delegate:self cancelButtonTitle:#"Yes" otherButtonTitles: #"No", nil];
alert.tag = 2;
[alert show];
[alert release];
// wait for confirm (0 or 1)
while (confirmed == -1) {
// this is what you need!!!
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
}
if (confirmed) {
[myObjects removeObjectAtIndex:index];
return YES;
}
else
return NO;
}
- (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
switch (alertView.tag) {
case 1:
// ...
break;
case 2:
if (buttonIndex == 0) // delete confirmed
confirmed = 1;
else // dismiss
confirmed = 0;
break;
default:
break;
}
}
#end

Related

UIAlertViewStylePlainTextInput - Program continues without waiting for text input from user

I'm sort of stuck on a UIAlertViewStylePlainTextInput. (My code is too long to post here, but this is the part where the problem exists.) It all sort of works, but the program doesn't wait until the user enters the information before continuing. The "editName" button doesn't change the display in the code below, but the updateDisplay button works fine (if you've pressed the "editName" button before you pressed "updateDisplay"). How can I get everything to sort of halt until the user enters the info (or cancels). Or is there some other lead that will take me where I need to go. (I've been toying with putting the rest of the code in the -(void) alertView, but that just doesn't seem to be the correct way of doing it).
Thanks in advance.
#import "HSTestViewController.h"
#implementation HSTestViewController
#synthesize display;
NSString *newName;
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (buttonIndex == 1)
{
newName = [[alertView textFieldAtIndex:0] text];
}
}
- (void) getNewName;
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"You made the High Score List"
message:#"Please enter your name"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Ok", nil];
alert.alertViewStyle = UIAlertViewStylePlainTextInput;
[alert show];
}
- (IBAction)editName:(id)sender
{
[self getNewName];
newName = display.text;
}
- (IBAction)updateDisplay:(id)sender
{
display.text = newName;
}
#end
Add UIAlertViewDelegate to the class. And implement the below delegate method.
OK button index value will be 1 in your case.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex)
{
// call the method or the stuff which you need to perform on click of "OK" button.
}
}
Hope this helps.

Get immediate value from UIALertView

I have what can be most accurately described as a Factory, which is generating some NSOperations. Before the NSOPerations are generated, I would like to check the current network status and, if the user is on a 3G/Mobile connection, warn them that they are about to do a data-heavy operation.
I attempted to do this with a UIAlertView, but the only way I can see to get the "response" from a UIAlertView is via the event-based delegate system. I was wondering if there was any way to have it act like the "confirm" dialogue in JavaScript, where it blocks the UI and I can get an immediate value from it once it is dismissed.
Is there any standard way to do this, or some example code I could be pointed towards that accomplishes something similar?
Blocking the main thread is considered bad practice on iOS, and thus there is no synchronous API for UIAlertView.
You should implement a delegate callback for the alert that enqueues the relevant NSOperation. It may be useful to subclass UIAlertView to store the relevant data you need to enqueue the NSOperation, or better yet store a block that captures the relevant variables and then just execute that when the user confirms the dialog.
You can implement something similar to that, using blocks. The execution will continue as in all other cases, but the flow of reading your code might more resemble what you want. Here is a helper class that I made for that purpose so that I can just go:
[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
Using the code of Ricky Helgesson, I've built a Pod component to use this solution easily in any project that uses CocoaPods.
https://github.com/nmaletm/STAlertView
The code that you should use is:
[[STAlertView alloc] initWithTitle:#"Title of the alert"
message:#"Message you want to show"
cancelButtonTitle:#"No" otherButtonTitles:#"Yes"
cancelButtonBlock:^{
// Code todo when the user cancel
...
} otherButtonBlock:^{
// Code todo when the user accept
...
}];
And add at the Podfile:
pod "STAlertView"
There are more instructions at the github page.

Detecting button pressed when there are multiple alert views

I have multiple alert views in one view, and I use this code to detect which button was pressed:
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
NSString *title = [alertView buttonTitleAtIndex:buttonIndex];
if ([title isEqualToString:#"OK"]) {
//for one alert view
[passCode becomeFirstResponder];
} else if ([title isEqualToString:#" OK "]) {
//for another alert view, had to change "OK" to " OK "
[passCodeConfirm becomeFirstResponder];
}
}
Now since there are multiple alert views in one view that do different things, I have to trick the user into thinking "OK" and " OK " are the same thing. It works and looks fine, but it feels kind of messy. Surely there is another way to do this, such as making this specific to an alert view, and then making it specific to another. Do you know how I would do this? Thanks!
It would be more technical as well better that set unique tag for separate UIAlertView and identify it and access in its delegate method.
For example,
UIAlertView *alert=[[UIAlertView alloc]initWithTitle:#"Message" message:#"Are You Sure you want to Update?" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Ok",nil];
[alert setTag:1];
[alert show];
[alert release];
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if(alertView.tag == 1)
{
// set your logic
}
}
Use tag property to uniquely identify each of the alertview u create.
Like this
myAlertView.tag = 1
Then in the clickedButtonAtIndex delegate method check which alertview's button was clicked using this tag property ,
if(alertView.tag==1)
I wouldn't use the titles to distinguish between the buttons. You'll run into problems when your app is localized or you decide to change the button titles, but forget to update them everywhere. Use the button indexes instead or if you only have one button in addition to a cancel button, use the cancelButtonIndex property of UIAlertView.
To distinguish between multiple alert views, you could use their tag property.
In your view, add a property for each alert view.
UIAlertView *myAlertType1;
UIAlertView *myAlertType2;
#property (nonatomic, retain) UIAlertView *myAlertType1;
#property (nonatomic, retain) UIAlertView *myAlertType2;
Create your alert using these properties
self.myAlertType1 = [[[UIAlertView alloc] initWithTitle: ... etc] autorelease];
[self.myAlertType1 show];
Then in your delegate method:
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (alertView == myAlertType1) {
// check the button types and add behaviour for this type of alert
} else if (alertView == myAlertType2 {
// check the button types and add behaviour for the second type of alert
}
}
Edit: Although the above works, iApple's suggestion of using the tag seems cleaner/simpler.
//in your .h file
UIAlertView* alert1;
UIAlertView* alert2;
//in your .m file
// when you are showing your alerts, use
[alert1 show]; //or
[alert2 show];
//and just check your alertview in the below method
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if(alertView == alert1)
{
//check its buttons
}
else //check other alert's btns
}

Simple question on UIAlertView and UIAlertViewDelegate

I hope this is pretty straight-forward. As you'll see by my code, I'm simply trying to get a UIAlertView button-press to pop me back to the root view.
I don't get any compile errors or warnings, and when I run the app, the "RedeemCoupon" method is called in the IBAction, and the UIAlertView pops up as it should, but it doesn't seem the "doneRedeeming" method gets called at all - I don't see anything from NSLog (yes I'm aware that I am setting buttonIndex to 0 - once I get this working I'll fix it). So, basically it doesn't work. I click the "cancel" button and the alert just goes away.
By the way I'm not sure if this matters, but this "RedeemCouponViewController" view is number 4 on the stack, and it was added by use of presentModalViewController in the previous view.
I'm open to other ways of doing this if needed - all suggestions welcome!
Thanks in advance!
// RedeemCouponViewController.h
#interface RedeemCouponViewController : UIViewController <UIAlertViewDelegate> {
// RedeemCouponViewController.m
- (IBAction) redeemYes: (UIButton*) sender {
CouponRedeem *redeem = [[CouponDatabase database] couponRedeem:_uniqueId];
[redeem release];
UIAlertView *doneRedeeming = [[UIAlertView alloc]initWithTitle:#"Coupon Redeemed!"
message:#"Thanks for shopping!"
delegate:self
cancelButtonTitle:#"Back to Main Menu"
otherButtonTitles:nil];
[doneRedeeming show];
[doneRedeeming release];
}
-(void) doneRedeeming: (UIAlertView *) doneRedeeming clickedButtonAtIndex: (NSInteger)buttonIndex {
if (buttonIndex = 0) {
NSLog(#"doneRedeemed method called");
[self.navigationController popToRootViewControllerAnimated:YES];
} else {
//do nothing
}
}
You want to have
if (buttonIndex == 0) {
in place of
if (buttonIndex = 0) {
The former checks for equality whereas the latter assigns.
Also, you want to have
– alertView:clickedButtonAtIndex:
where you have
- doneRedeeming:clickedButtonAtIndex:
You need to use UIAlertViewDelegate methods:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {}
not
-(void) doneRedeeming: (UIAlertView *) doneRedeeming clickedButtonAtIndex: (NSInteger)buttonIndex {}
Use the delegate method -alertView:didDismissWithButtonIndex: to listen for your cancel button index
#PengOne's answer is correct: your problem is this:
if (buttonIndex = 0) {
You said
I know it's not correct, but I just
wanted to be sure the statement was
true for now...
But buttonIndex = 0 evaluates to 0, making it equivalent to
if (0)
The code within that block will never execute, regardless of the value of buttonIndex. If you really want to do it unconditionally, change the if to if( 1 ), or just take the if out.
This would have been trivial to spot if you ran this code in the debugger. You might think you know what your code is doing, but if you don't watch it run, you don't.

Using OCUnit to test if an UIAlertView is presented

I'm working on an app that will display a UIAlertView upon hitting it's exit button, only if progress in the game has been made. I was wondering how you would use OCUnit to intercept the UIAlertView and interact with it, or even detect if it has been presented. The only thing I can think of is to monkeypatch [UIAlertViewDelegate willPresentAlertView], but that makes me want to cry.
Does anyone know of a better method of doing this?
Update: See my blog post How to Unit Test Your Alerts and Action Sheets
The problem with my other answer is that the -showAlertWithMessage: method itself is never exercised by unit tests. "Use manual testing to verify it once" isn't too bad for easy scenarios, but error handling often involves unusual situations that are difficult to reproduce. …Besides, I got that nagging feeling that I had stopped short, and that there might be a more thorough way. There is.
In the class under test, don't instantiate UIAlertView directly. Instead, define a method
+ (Class)alertViewClass
{
return [UIAlertView class];
}
that can be replaced using "subclass and override." (Alternatively, use dependency injection and pass this class in as an initializer argument.)
Invoke this to determine the class to instantiate to show an alert:
Class alertViewClass = [[self class] alertViewClass];
id alert = [[alertViewClass alloc] initWithTitle:...etc...
Now define a mock alert view class. Its job is to remember its initializer arguments, and post a notification, passing itself as the object:
- (void)show
{
[[NSNotificationCenter defaultCenter] postNotificationName:MockAlertViewShowNotification
object:self
userInfo:nil];
}
Your testing subclass (TestingFoo) redefines +alertViewClass to substitute the mock:
+ (Class)alertViewClass
{
return [MockAlertView class];
}
Make your test class register for the notification. The invoked method can now verify the arguments passed to the alert initializer and the number of times -show was messaged.
Additional tip: In addition to the mock alert, I defined an alert verifier class that:
Registers for the notification
Lets me set expected values
Upon notification, verifies the state against the expected values
So all my alert tests do now is create the verifier, set the expectations, and exercise the call.
The latest version of OCMock (2.2.1 the at time of this writing) has features that make this easy. Here's some sample test code that stubs UIAlertView's "alloc" class method to return a mock object instead of a real UIAlertView.
id mockAlertView = [OCMockObject mockForClass:[UIAlertView class]];
[[[mockAlertView stub] andReturn:mockAlertView] alloc];
(void)[[[mockAlertView expect] andReturn:mockAlertView]
initWithTitle:OCMOCK_ANY
message:OCMOCK_ANY
delegate:OCMOCK_ANY
cancelButtonTitle:OCMOCK_ANY
otherButtonTitles:OCMOCK_ANY, nil];
[[mockAlertView expect] show];
[myViewController doSomething];
[mockAlertView verify];
Note: Please see my other answer. I recommend it over this one.
In the actual class, define a short method to show an alert, something like:
- (void)showAlertWithMessage:(NSString message *)message
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil
message:message
delegate:self
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}
For your test, don't test this actual method. Instead, use "subclass and override" to define a spy that simply records its calls and arguments. Let's say the original class is named "Foo". Here's a subclass for testing purposes:
#interface TestingFoo : Foo
#property(nonatomic, assign) NSUInteger countShowAlert;
#property(nonatomic, retain) NSString *lastShowAlertMessage;
#end
#implementation TestingFoo
#synthesize countShowAlert;
#synthesize lastShowAlertMessage;
- (void)dealloc
{
[lastShowAlertMessage release];
[super dealloc];
}
- (void)showAlertWithMessage:(NSString message *)message
{
++countShowAlert;
[self setLastShowAlertMessage:message];
}
#end
Now as long as
your code calls -showAlertWithMessage: instead of showing an alert directly, and
your test code instantiates TestingFoo instead of Foo,
you can check the number of calls to show an alert, and the last message.
Since this doesn't exercise the actual code that shows an alert, use manual testing to verify it once.
You can get unit tests for alert views fairly seamlessly by exchanging the 'show' implementation of UIAlertView. For example, this interface gives you some amount of testing abilities:
#interface UIAlertView (Testing)
+ (void)skipNext;
+ (BOOL)didSkip;
#end
with this implementation
#import <objc/runtime.h>
#implementation UIAlertView (Testing)
static BOOL skip = NO;
+ (id)alloc
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method showMethod = class_getInstanceMethod(self, #selector(show));
Method show_Method = class_getInstanceMethod(self, #selector(show_));
method_exchangeImplementations(showMethod, show_Method);
});
return [super alloc];
}
+ (void)skipNext
{
skip = YES;
}
+ (BOOL)didSkip
{
return !skip;
}
- (void)show_
{
NSLog(#"UIAlertView :: would appear here (%#) [ title = %#; message = %# ]", skip ? #"predicted" : #"unexpected", [self title], [self message]);
if (skip) {
skip = NO;
return;
}
}
#end
You can write unit tests e.g. like this:
[UIAlertView skipNext];
// do something that you expect will give an alert
STAssertTrue([UIAlertView didSkip], #"Alert view did not appear as expected");
If you want to automate tapping a specific button in the alert view, you will need some more magic. The interface gets two new class methods:
#interface UIAlertView (Testing)
+ (void)skipNext;
+ (BOOL)didSkip;
+ (void)tapNext:(NSString *)buttonTitle;
+ (BOOL)didTap;
#end
which go like this
static NSString *next = nil;
+ (void)tapNext:(NSString *)buttonTitle
{
[next release];
next = [buttonTitle retain];
}
+ (BOOL)didTap
{
BOOL result = !next;
[next release];
next = nil;
return result;
}
and the show method becomes
- (void)show_
{
if (next) {
NSLog(#"UIAlertView :: simulating alert for tapping %#", next);
for (NSInteger i = 0; i < [self numberOfButtons]; i++)
if ([next isEqualToString:[self buttonTitleAtIndex:i]]) {
[next release];
next = nil;
[self alertView:self clickedButtonAtIndex:i];
return;
}
return;
}
NSLog(#"UIAlertView :: would appear here (%#) [ title = %#; message = %# ]", skip ? #"predicted" : #"unexpected", [self title], [self message]);
if (skip) {
skip = NO;
return;
}
}
This can be tested similarly, but instead of skipNext you'd say which button to tap. E.g.
[UIAlertView tapNext:#"Download"];
// do stuff that triggers an alert view with a "Download" button among others
STAssertTrue([UIAlertView didTap], #"Download was never tappable or never tapped");