I am new to iOS, and still learning the proper ways of how to do things. Another question that just reared its head, how can I cease execution of a method, based on a conditional and return back to calling code? Normally in PHP, I simply return true/false, or throw an exception, rather than tucking huge blocks of code within nested conditionals, but in iOS, I am not allowed to return from a method with a IBAction return signature.
What would be the preferred way of handling this sitch?
- (IBAction)submitCode:(id)sender
{
if ([codeEntry.text length] == 0) {
UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"Form Validation" message:#"No code entered, please try again."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[message show];
// IOS not allowing this
return NO;
}
// Prefer not to wrap the rest of the logic in an else, rather just cease
// execution and return back to calling code
NSLog(#"I would have submitted!");
}
You need to call return;, not return NO;.
The IBAction is really void. This means the method has no return value.
Just use
return;
This will stop execution of the current method.
Since IBAction is secretly typdef'ed to void, you can't return anything. Nothing is nothing is nothing, so just use return;.
Related
I've an observer pattern on the UI that checks what's the status of an object that handles a server connection that's trying to update a certain field on a database.
The UI's update method receives an object containing data pairs containing the information of what's happening with the connection. The problem is that I'm getting tangled with a lot of ifs checking for different possibilities.
- (void) update:(Bundle *)arg
{
if ([[arg getData:#"updatee"] isEqualToString:#"email"]){
UITableViewCell *emailCell = [[self tableView] cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
if ([[arg getData:#"connecting"] isEqualToString:#"true"]) {
//Email is being posted
[_emailLabel_email setText:#"Connecting..."];
[_emailLabel_set setHidden:YES];
emailCell.accessoryType = UITableViewCellAccessoryNone;
[_emailActivityIndicator startAnimating];
}else{
if ([[arg getData:#"succesfull"] isEqualToString: #"false"])
//Email was posted unsuccesfully
[[[UIAlertView alloc] initWithTitle:#"Taken Email Address"
message:#"The email address that you entered is already in use, please double check it"
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil] show];
else{
//Email was posted succesfully.
[_emailLabel_set setText:#"Change"];
}
[_emailActivityIndicator stopAnimating];
[_emailLabel_email setText:[mng getEmail]];
[_emailLabel_set setHidden:NO];
emailCell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
}
//Password cases
}
}
As the server responds with a string I'm finding difficult to avoid this spaggetti of code.
Which would be the smarter object to send on the update method?
You could keep the data how it is and put all of the values you expect for [arg getData:#"updatee"] as the keys in a dictionary. The values would be the string representation of method selectors, where each of the methods handles one of the cases in your current if statement. Now your update: method just gets the selector string from the dictionary, converts it to a real selector (NSSelectorFromString) and calls the method (passing the arg as a parameter, all methods used here must have matching parameter listing). If you receive an unexpected update type nothing would happen so you should log / assert that. If you get an exception while translating the selector you probably have a typo (good candidate for a test which executes all of the methods in the configuration).
Of course you could basically do the same thing with all the methods and a dirty big if / else statement.
XCode 4.5, iPad development, iOS6
Hi, I hope you can help a novice developer! Apologies in advance if this has already been answered but I could not find during my searches!
I am developing an app that needs to import a large amount of data into Core Data. The import routine works fine (alert shows 'Please wait' with activity monitor while routine works in the background) but I want to give the users more detailed feedback on the progress of the import (such as 'XX% imported'). The following code kicks the process off and -
- (IBAction)import:(id)sender{
[self showWaiting];
[self performSelectorInBackground:(#selector(callGrouper)) withObject:nil];
}
-(void)showWaiting{
alertMsg = #"Please Wait....";
waitAlert = [[UIAlertView alloc] initWithTitle:alertMsg message:nil delegate:self cancelButtonTitle:nil otherButtonTitles: nil];
[waitAlert show];
UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
indicator.center = CGPointMake(waitAlert.bounds.size.width / 2, waitAlert.bounds.size.height - 50);
[indicator startAnimating];
[waitAlert addSubview:indicator];
}
-(void)callGrouper{
ImportRoutine *firstTest = [[ImportRoutine alloc] init];
[firstTest runImport:managedObjectContext];
[waitAlert dismissWithClickedButtonIndex:0 animated:TRUE];
UIAlertView *alert = [[UIAlertView alloc]initWithTitle: #"iPad Application"
message: #"Import complete!"
delegate: self
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[alert show];
}
Within ImportRoutine (separate class) I have code that gathers data on percentage imported but how can I pass this message back to the main thread so I can update 'alertMsg' and in turn update the UIAlertView?
You can dispatch blocks of code back onto the main thread using GCD (grand central dispatch):
dispatch_async(dispatch_get_main_queue(), ^{
// code here to update UI
});
Any object in the scope of the method that contains the dispatch call gets retained which makes it easy to pass objects back into the main thread without worrying about the background thread being deallocated along with its objects before you've had a chance to process the data. Primitive values in the local scope (aka int, float, double, etc) are copied, so if you set an int to 5, dispatch a block where you print the value of the int, and then right after set the int to 10, even if the block executes after you set the int to 10 it'll still print 5. Note that You can't mutate the same mutable object (such as `NSMutableArray or NSMutableDictionary) in two threads at the same time or mutate in one and enumerate in another without crashing so you'll want to be careful about doing something like that (thanks goes to #andrewmadsen for reminding me to warn you).
dispatch_async(), unlike dispatch_sync(), will not wait for the code that's dispatched to complete before continuing execution which is nice since your background thread doesn't need to care if things in the UI have finished.
You could stick the dispatch call inside of the method on the ImportRoutine class that calculates the progress as long as your UIAlertView is addressable outside of your view controller class. Or if you want to follow model-view-controller design principals more closely, you could create a method like so in your view controller:
- (void)updateProgressToPercentComplete:(double)percent {
if ([NSThread currentThread] != [NSThread mainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
// update code or call to method that is guaranteed to be on the main thread.
}
}
else {
// update code or call to method that is guaranteed to be on the main thread.
}
}
If you've gone into the documentation and now you're all like "oh my gosh Objective-C blocks are the coolest thing ever" you could modify the method above so you don't need to write the same update code twice:
- (void)updateProgressToPercentComplete:(double)percent {
void (^updateProgressBlock)(void) = ^{
// update code
};
if ([NSThread currentThread] != [NSThread mainThread]) {
dispatch_async(dispatch_get_main_queue(), updateProgressBlock());
}
else {
updateProgressBlock();
}
}
By the way I noticed in your -callGrouper code that you're using an existing managedObjectContext that I assume you created on the main thread in a background thread... most of core data isn't threadsafe so you need to be extremely careful or you will crash all over the place. You might be better off creating a secondary managed object context on the background thread and then merging changes into the context on the main thread (or save on the background thread and re-fetch on the main thread).
Edit:
Basic flow: Begin your background process from your view controller and pass in a progress block. -> Import class in the background thread executes your progress block periodically -> Inside your progress block you dispatch back to the main thread to update UI.
In your ImportRoutine class add a property declaration like so:
#property (nonatomic, strong) void (^progressBlock)(NSUInteger);
Which means a property called progressBlock that takes an unsigned integer (0-100) and doesn't return anything (void). You should make this property private by using a class extension.
Then you'll want to create a method in your import class like so:
- (void)callGrouper:(void (^)(NSUInteger))progress {
[self setProgressBlock:progress];
// Your import code
}
In your method where you receive progress updates, call the progressBlock and pass in your progress as a number between 0 and 100:
if ([self progressBlock] != nil) {
[self progressBlock](progressValue);
}
Notice that I check to make sure the progress block isn't nil. You would crash and burn if you tried to execute a NULL block.
Then you can pass in a block as the object in your import routine call you already have in the view controller and inside the block dispatch back to the main queue and update your progress.
You can use:
[self performSelectorOnMainThread:#selector(yourSelector) withObject:anObjectIfYouNeedToSendOne waitUntilDone:YES/NO];
The UI runs on main thread and so you cand acces again your UIAlertView or other UI object.
Here is my code that calls "displayAlert". The problem is not only do I get an error message (wait_fences: failed to receive reply: 10004003) but the "alert" is displayed twice!
if(gSiteID.globalSiteID.length == 0) { // user didn't choose site
[self displayAlert:NSLocalizedString(#"Missing Site ID", nil) andData:NSLocalizedString(#"You must choose a site from the View Sites page",nil)];
return;
}
This the code for "displayAlert":
- (void) displayAlert: (NSString *) title andData: (NSString *) errorMsg {
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle: title
message: errorMsg
delegate:nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
return;
}
I have searched SO and Google and found nothing that is specific to my issue. What am I doing wrong?
Are you testing this on a real device or the simulator? wait_fences: failed to receive reply usually means something bad happened with the debugger's connection to your device, or that you sat at a breakpoint for a really long time and it timed out. Are you sure that the code only executes once, and that nothing else could call that method? Stick breakpoints in your if statement and in your displayAlert:andData: method and see what happens. Run through your logic and find all the cases when that display alert method can be called and stick breakpoints on all of them.
I found the problem: indeed I was calling it twice from different .cs files (do you see the egg on my face?). Jack Lawrence please post your answer to the question, since you hit it on the head.
I'm having trouble finding examples of the correct way to use NSError, UIAlertView, and NSErrorRecoveryAttempting together on iOS. Most of the documentation and examples I can find cover the equivalent functionality on OS X, where the relevant behaviors are integrated by Cocoa. But in iOS it seems to be necessary do do this "by hand", and I can't find good examples of how it's done.
I'd very much appreciate a few examples of best practice in using information in NSError to support recovery attempts from NSErrors reported to the user.
According to Apple's documentation:
Important: The NSError class is available on both Mac OS X and iOS. However, the error-responder and error-recovery APIs and mechanisms are available only in the Application Kit (Mac OS X).
So, I'm not sure if you can use NSErrorRecoveryAttempting even though it does appear to be defined in the documentation (it looks like this is an area of the UIKit docs that have not yet been updated after being copied from AppKit's documentation).
Here is how I handle errors in my code:
NSError *error = nil;
id result = [SomeClass doSomething:&error];
if (!result) {
NSLog(#"Do something failed: %#", error);
UIAlertView *alert = [[[UIAlertView alloc] initWithTitle:#"Something failed!" message:#"There was an error doing something." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil] autorelease];
[alert show];
return;
}
I found a great example of this.
See the following blog post and GitHub code (including sample project) by James Beith
http://www.realmacsoftware.com/blog/cocoa-error-handling-and-recovery
https://github.com/realmacsoftware/RMErrorRecoveryAttempter
I was able to successfully use this on the iPhone simulator.
I'm trying to mirror AppKit's error handling mechanism in UIKit, mainly because I want to take advantage of the responder chain to forward errors upwards. I haven't tested this fully, but at the moment it's looking like below.
It reflects AppKit pretty closely, but the will/did hooks can be overridden to perform custom error presentation and recovery respectively. The default behaviour is to show a UIAlertView for presentation and use a psuedo-NSErrorRecoveryAttempting object for recovery.
#implementation UIResponder (ErrorHandling)
- (void)presentError:(NSError *)error
completion:(void (^)(BOOL recovered))completion
{
if (nil == (error = [self willPresentError:error])) {
return;
}
if (self.nextResponder) {
[self.nextResponder presentError:error completion:completion];
return;
}
// Code to create and show UIAlertView
// e.g. https://github.com/jayway/CWUIKit/blob/master/Classes/UIAlertView%2BCWErrorHandler.m
// The UIAlertViewDelegate calls didPresentError...
}
/*
Override to customise the error object as in AppKit.
You can also perform your own error presentation, and return nil to terminate the default handling.
Custom error presentation UI should still call didPresentError... when dismissed
*/
- (NSError *)willPresentError:(NSError *)error
{
return error;
}
/*
Override to perform custom error recovery.
*/
- (void)didPresentError:(NSError *)error optionIndex:(NSInteger)optionIndex completion:(void (^)(BOOL recovered))completion
{
id recoveryAttempter = [error recoveryAttempter];
if ([recoveryAttempter respondsToSelector:#selector(attemptRecoveryFromError:optionIndex:completion:)]) {
[recoveryAttempter attemptRecoveryFromError:error optionIndex:optionIndex completion:completion];
}
}
#end
I want to put in a timeout in case it takes too long to find my location, send out the relevant url, and parse the xml. It worked when I used performSelector:withObject:afterDelay in the locationManager (just to test getting the xml), but when I put similar code around my parser it doesn't actually abort the parsing. I am testing this by dropping the delay to 0.01.
My problem is: even with the delay set to 0.01, it still waits for all the parsing to complete first, and only then does it put up the alertView that is coded in the parsingDidTimeout method.
I did try this with a timer, and that wasn't working as well as performSelector: does in the other parts of my code. Either way, it doesn't put up the alertView, and stop the parsing, until after the parsing has finished, no matter how long that takes.
I create a url which requires a radius. First I try a small radius, but if I don't get the data I need, I expand the radius and send the url again and parse again. Here is part of my StartParsing method.
xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];
XMLParser *parser = [[XMLParser alloc] initXMLParser];
[xmlParser setDelegate:parser];
if (!hadToExpandRadius){//meaning, only do this the first time I send out the url and parse
[self performSelector:#selector(parsingDidTimeout:) withObject:nil afterDelay:0.01];
}
//Start parsing the XML file.
BOOL success = [xmlParser parse];
if(success){
if((didNotGetTheDataYet) && (radius < 500)){
hadToExpandRadius = YES;
radius = radius + 35;
[self startParsing];//do this same method, with larger radius
}
else {
NSLog(#"No Errors");
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(parsingDidTimeout:) object:nil];}
[parser release];
}
-(void)parsingDidTimeout{
[xmlParser abortParsing];
UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:#"Try Later" message:#"We need a better connection. We can get the data later." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[servicesDisabledAlert show];
[servicesDisabledAlert release];
[myActivityView stopAnimating];
}
Thank you for your help.
Calling performSelector:withObject:afterDelay: you ask the run loop to call the selector later. But [xmlParser parse] blocks the run loop, so it doesn't have a chance to call you selector.
abortParsing is designed to be called inside parsers' delegate methods.
The workaround can be to parse in a separate thread.
Found it -- just extra ":" in my performSelector:#selector(parsingDidTimeout:)!
I thought it was something fancy having to do with the second thread. Just syntax.
Thanks for explaining about the parse blocking the run loop. I was hoping not to need another thread, but your suggestion fixed my problem. Thanks.