Menu Navigation Menu

A while back, I built a little iPhone app called Tag Along. It's a relatively simple app that allows users to passively invite their friends to join an activity they have in common. It was my first iOS app and the idea was to build something as a way of teaching myself Objective-C and iOS development. Shortly after building it, I worked on a few other apps until I was put back on Rails projects.

I continued to fix bugs in my own app, but I didn't spend any time working on new features or following the progression of iOS. Fast-forward a couple years, and, with the increased potential in iOS projects, it seemed like a good idea to brush up on my skills.

So, I decided to learn about the changes in the ecosystem (ARC, storyboarding, etc.) and to see if I could update my app with any of these new technologies.

ARC, or Automatic Reference Counting, appears to be the future of Objective-C, so why not use it? Well, the first reason is that the library I use most in my app is ASIHTTPRequest, which is not compatible with ARC. The -fno-objc-arc compiler flag is a workaround option for those that don't want to upgrade libraries, but, since the author of ASIHTTPRequest said to stop using it and move on back in May of 2011, replacing the library seemed like a good idea regardless.

The question is, what to replace ASIHTTPRequest with? AFNetworking, MKNetworkingKit, and FSNetworking all seemed like viable options. After some research, I chose AFNetworking because it seemed to have both the best official and unofficial documentation. Plus, I found a great blog post by Will Larche called Converting from ASIHTTPRequest to AFNetworking. Although it wasn't exactly what I needed, it did give me a head start. This switch also gave me a chance to clean up my code by moving all my API calls into a single service file.

But, first, let's look at the old way:

NSString *urlString = [NSString stringWithFormat:@"%@events.json", BASE_WEB_SERVICE];
NSURL *url = [NSURL URLWithString:urlString];
__block ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request addBasicAuthenticationHeaderWithUsername:[[TAService sharedInstance]authToken] andPassword:nil];
[request setNumberOfTimesToRetryOnTimeout:2];
[request setCompletionBlock:^{
  NSString *responseString = [request responseString];
  self.events = [responseString yajl_JSON];
  [self.tblView reloadData];
  updatedLabel.text = [NSString stringWithFormat:@"Updated: %@", [dateFormatter stringFromDate:[NSDate date]]];
  [act stopAnimating];
}];

[request setFailedBlock:^{
  NSError *error = [request error];
  UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Network connection error." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
  [alert show];
  [alert release];
  [act stopAnimating];
}];

[request startAsynchronous];

TAService  was initially created as a singleton that would manage all the API calls. However, with my early (lack of) knowledge I didn't pursue this and instead simply used the service to manage the user's authorization token:

-(NSString*)authToken
{
  NSError *error = nil;
  return [STKeychain getPasswordForUsername:@"authToken" andServiceName:@"TagAlong" error:&error];
}

My goal is to simplify my calls in the view controllers so they are only dealing with making the call and handling the returned data, keeping authorization and error logic in a separate place. In the end, my view controller should have a call that looks something like this:

[[TAService sharedInstance] fetchTagAlongs:^(AFHTTPRequestOperation *operation, id responseObject) {
  self.events = responseObject;
  [self.tblView reloadData];
}

In order to get there, however, we need to set up a singleton for handling the authorization and initialization of our API call. My app uses a Rails backend for the API and I need to pass an authentication token to identify and authenticate the user. The unofficial AFNetworking docs provided this solution. So, I copied it and made some changes:

#import <Foundation/Foundation.h>
#import "AFHTTPClient.h"

@interface TAAuth : AFHTTPClient

+(TAAuth *)sharedManager;

@end
#import "TAAuth.h"
#import "AFJSONRequestOperation.h"
#import "AFNetworkActivityIndicatorManager.h"
#import "TAService.h"

@implementation TAAuth

- (id)initWithBaseURL:(NSURL *)url
{
  self = [super initWithBaseURL:url];

  if (!self) {
    return nil;
  }

  [self registerHTTPOperationClass:[AFJSONRequestOperation class]];
  [self setDefaultHeader:@"Accept" value:@"application/json"];
  [self setParameterEncoding:AFJSONParameterEncoding];
  [self setAuthorizationHeaderWithUsername:[[TAService sharedInstance] authToken] password:nil];

  [[AFNetworkActivityIndicatorManager sharedManager] setEnabled:YES];

  return self;
}

+(TAAuth *)sharedManager
{
  static dispatch_once_t pred;
  static TAAuth *_sharedManager = nil;

  dispatch_once(&pred, ^{ _sharedManager = [[self alloc] initWithBaseURL: [NSURL URLWithString:BASE_WEB_SERVICE]];
});
  return _sharedManager;
}

@end

TAAuth inherits from AFHTTPClient and I initialize the sharedManager  with the base url set, allowing me to make simple calls to the path passing in a completion block. I can then locate all the API calls in one place:

-(void)fetchTagAlongs:(TABlock)completionBlock
{
  [[TAAuth sharedManager] getPath:@"events.json" parameters:nil success: completionBlock failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    [self handleFailure:error];
  }];
}

handleFailure is used for any connection issues where an appropriate response is not returned from the API:

-(void)handleFailure:(NSError *)error
{
  [SVProgressHUD dismiss];
  NSLog(@"error: %@", error);
  UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Network connection error." delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
  [alert show];
}

Normally, I would use a failure block and handle the failure in the same context that I handle the successful completion, but this was unnecessary due to how the API is written (errors are returned as a success and handled in the completion block).

Following the above pattern I was able to go through all of my API calls and remove ASIHTTPRequest completely. At the same time, I cleaned up my code significantly and now have all my API calls in one place. This should make future changes or feature additions more manageable.

The next step from here is to determine what other libraries are extinct and whether or not they have decent replacements. I'll update them one by one until my app is completely modernized. Of course, with iOS 7 right around the corner, it will need a whole new design update, but that is for another day and another blog post.


Contact us for a complimentary 30 minute consultation.

get in touch