Often, in GUI, I'm faced with a program flow which is essentially sequential, but involves asynchronous operations (such as NSURLConnection
downloading stuff) and UI actions (wait for the user to choose an option in an UIActionSheet
).
Just to illustrate, for example,
UIActionSheet
, prompting the user to choose a color.UIAlertView
, wait for tap on OK)UIAlertView
, retry downloading.The flow is sequential - we don't do 2 before 1. But because of async operations and UI elements, this very quickly becomes spaghetti code if we use delegates (or blocks).
Is there a general approach to writing such code?
There is a library called Reactive Cocoa that is amazing, but tough to get used to.
An easier way to achieve your goal, but not quite as awesome, is using a blocks wrapper around UIAlertView and UIActionSheet. Also this assumes you have call back blocks in your network code.
Example:
- (void)showActionSheet
{
BlockActionSheet *sheet = [BlockActionSheet sheetWithTitle:@"Choose one"];
__weak BlockActionSheet *weakSheet = sheet;
[sheet addButtonWithTitle:@"Blue" atIndex:0 block:^{
[self downloadFileFromServerSuccessBlock:^{
//YAY
} failureBlock:^{
BlockAlertView *alert = [BlockAlertView alertWithTitle:@"Failure" message:@"Something Went Wrong"];
[alert addButtonWithTitle:@"Try Again" block:^{
[weakSheet showInView:self.view];
}];
[alert setCancelButtonWithTitle:@"Cancel" block:nil];
}];
}];
[sheet addButtonWithTitle:@"Black" atIndex:1 block:^{
//something else
}];
[sheet setCancelButtonWithTitle:@"Cancel" block:nil];
[sheet showInView:self.view];
}
So the last line "[sheet showInView:self.view]" starts the whole thing off. If they select Blue then that block gets called. Your network code also supports blocks, so you get a success and failure block callback from there. If it's failure then you pop up a block based alert view that makes the ActionSheet show itself all over again.
I hope this helped at least a little. Also there are probably some strong referencing happening with the "self.view" call so i'd make a weak reference to the view as well.
Here is the ReactiveCocoa example. I am very new to this framework so I hope I am using it correctly.
/*
* This function is getting called from a login view controller.
*/
- (void)sendAuthentication
{
/*
* A RACSubscribable will invoke the where^ block when it calls [sendNext:];
* That call is coming from the ThaweQBRequestController.
* If the where^ block return YES then the subscribeNext^ block will get called.
* asMaybes wraps the object I send back in a RACMaybe object which has a convenience method [hasObject];
* The subscribeNext^ block has an object coming into it, that object is an array in this case that I sent
* from the ThaweQBRequestController.
*/
RACSubscribable *sub = [[ThaweQBRequestController logInWithUsername:self.thaweusername password:self.thawepassword] asMaybes];
[[sub where:^(id x) {
return [x hasObject];
}]
subscribeNext:^(id _) {
NSArray *array = [_ object];
NSString *errcode = [array objectAtIndex:0];
if (errcode.boolValue == YES)
//YAY SUCCESS!!!
} else {
//UH OH
}
// Update UI, the array has the username and password in it if I want to display or whatever is clever.
}];
}
Then we get into a network request controller I have.
+ (RACAsyncSubject *)logInWithUsername:(NSString *)username password:(NSString *)password
{
/*
* First we have our loginCommand. It will invoke the block we give it on a background thread.
* This block returns a Subscribable. We can subscribe other blocks to be invoked when the aync call is done.
* In this case we are creating loginResult to retrieve the async call.
*/
RACAsyncCommand *loginCommand = [RACAsyncCommand command];
/*
* Now we have our AsyncSubject. We are instantiated it here and are going to pass it back to the login view controller;
* When our async call finishes and we filter through the results we will call [subject sendNext:] to invoke blocks we created in
* the login view controller;
* We will filter the results uses the loginResult blocks.
*/
RACAsyncSubject *subject = [RACAsyncSubject subject];
/*
* loginResult is a "Subscribable". Every time it gets a [sendNext:] call it runs the blocks assosiated with it.
* These [sendNext:] calls are coming from our network code.
* 'repeat' means that even after the async block is invoked it keeps a reference to it incase we want to use it again.
* 'asMaybes' wraps a RACMaybe object around the object you send to the loginResult blocks. The benefit of a RACMaybe is
* that it has some convienence methods like 'hasError' and 'hasObject'.
*/
RACSubscribable *loginResult = [[[loginCommand
addAsyncBlock:^(id _) {
return [self authenticateUser:username password:password];
}]
repeat]
asMaybes];
/*
* The first block, where^, get called every time loginResult calls [sendNext:].
* If it returns YES then the select^ block gets called. The select^ block invokes the subscribeNext^ block and sends it the
* error.
*/
[[[loginResult
where:^(id x) {
return [x hasError];
}]
select:^(id x) {
return [x error];
}]
subscribeNext:^(id x) {
NSLog(@"network error omg: %@", x);
}];
/*
* Same as above except this time we are looking for instances when we have an object instead of an NSError.
* The object we are getting is being returned from our network code. In this case it is an integer, 0 means we had a successfull login.
* Now we can call [subject sencNext:] to inform our login view controller of the pass or fail of the authentication.
* [sendCompleted] is a cleanup call, allowing ReactiveCocoa to dispose of our blocks and free up memory.
*/
[loginResult
where:^(id x) {
return [x hasObject];
}]
subscribeNext:^(id _) {
NSNumber *number;
NSString *errcode = [_ object];
if (errcode.intValue == 0) number = [NSNumber numberWithBool:YES] ?: [NSNumber numberWithBool:NO];
[subject sendNext:[NSArray arrayWithObjects:number, username, password, nil]];
[subject sendCompleted];
}];
/*
* [execute:] starts the whole thing off. This call invokes the loginCommand block that returns the async call to the loginResult subscribable.
*/
[loginCommand execute:@"This value gets transfered to the addAsyncBlock:^(id _) block above."];
return subject;
}
/*
* This function uses a QuickBase wrapper I made to make server request and whatnot.
* This function is called when the [loginCommand execute:] call in the previous function gets called
* and invokes the loginCommand block which returns the async request.
* That request is what this function is returning.
*/
+ (RACAsyncSubject *)authenticateUser:(NSString *)username password:(NSString *)password
{
QBRequest *request = [[QBRequest alloc] init];
[request setQuickBaseAction:QBActionTypeAuthenticate];
[request setURLString:URLstring withDatabaseID:nil];
[request setApplicationToken:appToken];
return [request sendAndPersist:NO username:username password:password];
}
And now we are in my actually network wrapper that knows when a request has completed or failed or whatever.
- (RACAsyncSubject *)sendAndPersist:(BOOL)persist username:(NSString *)username password:(NSString *)password
{
self.subject = [RACAsyncSubject subject];
/*
* These next two blocks are called when my network request is done.
* My AsyncSubject is a property so that I can reference it later when I parse
* throught the response and figure out whether I logged in correctly or not.
* In the case that the network call itself fails, then the AysncSubject calls
* [sendError:] which will invoke those NSError capturing blocks in the ThaweQBRequestController.
*/
[anOp onCompletion:^(MKNetworkOperation *completedOperation) {
dispatch_async(background_parseSave_queue(), ^{
[self updateDatabase];
});
} onError:^(NSError *error) {
[subject sendError:error];
}];
[engine enqueueOperation:anOp];
return subject;
}
And finally to give you an idea of when I have the subject [sendNext:] is in my parser. The self.currentParsedCharacterData is an NSString with and integer value.
else if ([elementName isEqualToString:@"errcode"])
{
if ([self.action isEqualToString:@"API_Authenticate"]) {
[subject sendNext:[self.currentParsedCharacterData copy]];
[subject sendCompleted];
}
}
I know this is long but I really wanted to give some actual code examples.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With