Using MVCS to Group Multiple Connections in a Queue


#1

Awesome book. I would be totally lost without it. My entire app is based on the stuff I learned from it. Question on the following scenario:

I have an API that returns an array of ids in addition to some other info. These ids are used to form URLs that return other related objects. When a user taps a button, I need to grab the first object from one URL, construct additional URLs based on the contents of the array, then retrieve each of those related objects. Think of it like a user profile with an array of message ids that comprise her activity. I have the feeling that my current approach to this is really hacky. I use a for loop after the initially returned object to spawn a bunch of other connections. The Store and Connection Delegate are nearly identical to the examples in the book. Code from the calling table view controller below:

//Grab the initial user object with a single call

- (void)fetchUserProfile
{
    void (^completionBlock)(MYUserProfile *obj, NSError *err, int statusCode) = ^(MYUserProfile *obj, NSError *err, int statusCode) {
        
        if (!err && statusCode == 200) {
           //If all goes well, set the returned object as the ivar that drives the table view. Do not reload tableview as the activity messages are nothing but IDs at the moment. However, the MYUserProfile object contains stub MYUserActivityLogEntry objects with just an id

           myIVARforTheTableViewDataSource = obj;
           
           //Call the method below go grab each of the objects on my API corresponding to the IDs in the array within MYUserProfile
           [self fetchUserActivityFromArrayOfIDs];
        
        } else //Handle the error or incorrect status code with an UIlAlertView
     
    };
    
    [MYStore sharedStore] fetchUserProfileWithCompletion:completionBlock];
    
}

//method to grab all of the activity log messages which are seperate resources on my API
- (void)fetchUserActivityFromArrayOfIDs
{
    
    void (^completionBlock)(MYUserActivityLogEntry *obj, NSError *err, int statusCode) = ^(MYUserActivityLogEntry *obj, NSError *err, int statusCode) {
        
        if (!err && statusCode == 200) {
            //Hacky approach to determining when this is all done. Essentially, this creates a new array of any MYUserActivityLogEntry objects that are not complete. An activity object will not be complete if its actvityMessage property isn't set. 
 
            NSArray *completionCheckArray = [[myIVARforTheTableViewDataSource activityArray] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"activityMessage = %@", [NSNull null]]];
            
            if ([completionCheckArray count] == 0) {
                //If there is nothing in the completionCheckArray, then every activity object has its activityMessage property and we can finally reload the tableView to show the messages. 
                [self tableView] reloadData];
            }
                
        } else //Handle the error or incorrect status code with an UIlAlertView. 

    };
    
    //For Loop that grabs each of the stub activity objects in the IVAR, gets the ID, and uses the Store to create a connection. 
    for (MYUserActivityLogEntry *act in [myIVARforTheTableViewDataSource activityArray]) {
        [MYStore sharedStore] fetchActivityDetails:act withCompletion:completionBlock];
    }

}

I did some digging around and it seems that using an NSOperationQueue in conjunction with the MVCS approach will help me better handle the fact that I have to make multiple calls. For example, If even one of the MYUserActivityLogEntry calls returns a 404, I need to shut the whole thing down and not throw a UIAlertView for every subsequent failing call. The completion array + for loop seems a bit inefficient to me. My question is where should I set up the NSOperationQueue? Would I do it in the store? The (equivalent of) the BNRConnection object? Or do I set it up in the calling view controller? Or is the NSOperationQueue a bad idea? Thanks in advance.


#2

If you have control over your backend, one thing you can do is combine or batch your calls. For example, you could pass an optional flag to the initial user profile call which tells it to also return activity information. Alternatively, you could change your service to allow multiple activity ids in a single call so you’re not firing off so many individual requests. If you are going to make individual requests, you may want to consider displaying the responses to the user as they come in instead of waiting for them all to come back. In your current implementation if one of your requests times out, it will hold up all the other information you have waiting.

An NSOperationQueue essentially allows you to move your work off the main queue (thread) which is running the rest of your application. The advantage to this is that UI events are not held up by network events or other things you’re doing. As you mention, this also provides a convenient way to cancel all the operations in the queue. If you do use it, I would set it up in the store and enqueue work within your fetchUserProfileWithCompletion method. This way the handling of the response and firing of subsequent requests can also be done on that queue. There are some other things you can look at, like addOperations:waitUntilFinished: but this might be overkill. One thing you need to watch out for is that when you tell your table to reload its data, you must do that from the main queue. So once you’ve received your data, you must enqueue an operation onto the main queue that tells the UITableView what to do. Secondly, it’s not clear from here where you set data into your store (if you’re following the book, it might be distributed between your models and the store), but you’ll have to be sure that your second thread isn’t writing data in a way that isn’t thread safe. Overall, if your issue is with responsiveness in the app (i.e. preparing network requests and processing responses is holding up the UI) then using a secondary queue will help. If your concern is cleanliness of your code, then I would look elsewhere (like encapsulating the chain of requests inside another object).


#3

Thanks for the direction! This is super helpful. I will post up my code once its done. One quick question, I have decided to use my store to manage the model objects. Mostly because there are many background calls to the API that end up updating the models, regardless of what the user is doing with the UI. Each view controller that needs a model object maintains an ivar pointer to the store’s property. I use the viewWillAppear method to reload the tableView datasource so that it always has the lastest version of the model from the store. Thus, it is really necessary that I return the objects in the completion blocks to the calling view controller? Maybe I should just use a success BOOL to let the VC know whether the call was successful or not? I have it working this way now, but am curious if there are any hidden gotchas with this approach. Essentially, is it advisable to let my store manage my models? Totally understand if this is impossible to answer without my code.


#4

I wanted to close the loop on this in case another BNR disciple has the same questions. So here is the solution…AFNetworking. Specifically, subclass the HTTPClient class (which becomes my new store) and use the enqueueBatchOfHTTPRequestOperations for the queue management.

When I started learning how to build iOS apps I was adamant that I not use third party frameworks. I wanted to actually learn how to do this and more often than not, using a third party framework led to more headache than utility. Not because the frameworks didn’t work - mostly because I did not have the skills necessary to understand how to use them. However, after learning both Objective-C and iOS development from the BNR books, building my own MVCS architecture, I am finally able to understand the frameworks enough to actually use them. If anyone comes across this post thinking that AFNetworking is an easy way to get the desired functionality without knowing how to build it yourself, I strongly advise otherwise. Even with all that AFNetworking provides, I still had to build a store, understand and effectively use completion blocks, and integrate it into my app.

Bottom line - Build your own, make it work, then if necessary, use a 3rd party framework only if it accelerates what you already know. Do not let it supplant the process of learning.