Serial flow of execution with asynchronous operations and user interface

Often in the GUI, I came across a program stream that is essentially sequential, but includes asynchronous operations (e.g. NSURLConnection loading material) and user interface actions (wait for the user to select an option in the UIActionSheet ).

Just to illustrate, for example,

  • Show UIActionSheet , prompting the user to select a color.
  • If the color is blue, download the file from the server.
    • If the download fails, inform the user ( UIAlertView , wait while click OK)
    • If the user selects YES in UIAlertView , retry the download.
  • If the color is black, do something else.
  • etc. etc.

The flow is consistent - we do not do 2 to 1. But because of asynchronous operations and user interface elements, this very quickly becomes spaghetti code if we use delegates (or blocks).

Is there a general approach to writing such code?

+6
source share
1 answer

There is a library called Reactive Cocoa that is awesome but hard to get used to.

An easier way to achieve your goal, but not so awesome, is using cover locks around the UIAlertView and UIActionSheet. It is also assumed that you have reverse blocks in the 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 of β€œ[sheet showInView: self.view]” starts everything. If they select Blue, then this block is called. Network code also supports blocks, so you get a successful and fail-safe callback from there. If this fails, you will see a blocking pop-up warning that causes the ActionSheet to show itself again and again.

Hope this helps at least a little. There are also probably some strong links occurring with the call to "self.view", so I will also make a weak link to the view.

Here is an example of ReactiveCocoa. I am very new to this structure, so I hope that I use 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 to the network request controller that 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 actual network shell, which knows when the request completed or failed or something else.

 - (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 [sendNext:] item in my parser. Self.currentParsedCharacterData is an NSString value with an integer 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 a long time, but I really wanted to give some code examples.

+9
source

Source: https://habr.com/ru/post/923433/


All Articles