AFNetworking 2.0 Tutorial

Learn how to easily get and post data from a web service in iOS in this AFNetworking 2.0 tutorial. By Joshua Greene.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

A Little Weather Flair

Hmm, that looks dreary, like a week’s worth of rainy days. How could you jazz up the weather information in your table view?

Take another peak at the JSON format from before, and you will see that there are image URLs for each weather item. Displaying these weather images in each table view cell would add some visual interest to the app.

AFNetworking adds a category to UIImageView that lets you load images asynchronously, meaning the UI will remain responsive while images are downloaded in the background. To take advantage of this, first add the category import to the top of WTTableViewController.m:

#import "UIImageView+AFNetworking.h"

Find the tableView:cellForRowAtIndexPath: method and paste the following code just above the final return cell; line (there should be a comment marking the spot):

 cell.textLabel.text = [daysWeather weatherDescription];
    
    NSURL *url = [NSURL URLWithString:daysWeather.weatherIconURL];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    UIImage *placeholderImage = [UIImage imageNamed:@"placeholder"];
    
    __weak UITableViewCell *weakCell = cell;
    
    [cell.imageView setImageWithURLRequest:request
                          placeholderImage:placeholderImage
                                   success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
                                       
                                       weakCell.imageView.image = image;
                                       [weakCell setNeedsLayout];
                                       
                                   } failure:nil];

UIImageView+AFNetworking makes setImageWithURLRequest: and several other related methods available to you.

Both the success and failure blocks are optional, but if you do provide a success block, you must explicitly set the image property on the image view (or else it won’t be set). If you don’t provide a success block, the image will automatically be set for you.

When the cell is first created, its image view will display the placeholder image until the real image has finished downloading.

Now build and run your project. Tap on any of the operations you’ve added so far, and you should see this:

Nice! Asynchronously loading images has never been easier.

A RESTful Class

So far you’ve been creating one-off networking operations using AFHTTPRequestOperation.

Alternatively, AFHTTPRequestOperationManager and AFHTTPSessionManager are designed to help you easily interact with a single, web-service endpoint.

Both of these allow you to set a base URL and then make several requests to the same endpoint. Both can also monitor for changes in connectivity, encode parameters, handle multipart form requests, enqueue batch operations, and help you perform the full suite of RESTful verbs (GET, POST, PUT, and DELETE).

“Which one should I use?”, you might ask.

  • If you’re targeting iOS 7 and above, use AFHTTPSessionManager, as internally it creates and uses NSURLSession and related objects.
  • If you’re targeting iOS 6 and above, use AFHTTPRequestOperationManager, which has similar functionality to AFHTTPSessionManager, yet it uses NSURLConnection internally instead of NSURLSession (which isn’t available in iOS 6). Otherwise, these classes are very similar in functionality.

In your weather app project, you’ll be using AFHTTPSessionManager to perform both a GET and PUT operation.

Note: Unclear on what all this talk is about REST, GET, and POST? Check out this explanation of the subject – What is REST?

Update the class declaration at the top of WTTableViewController.h to the following:

@interface WTTableViewController : UITableViewController<NSXMLParserDelegate, CLLocationManagerDelegate, UIActionSheetDelegate>

In WTTableViewController.m, find the clientTapped: method and replace its implementation with the following:

- (IBAction)clientTapped:(id)sender 
{
    UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"AFHTTPSessionManager"
                                                             delegate:self
                                                    cancelButtonTitle:@"Cancel"
                                               destructiveButtonTitle:nil
                                                    otherButtonTitles:@"HTTP GET", @"HTTP POST", nil];
    [actionSheet showFromBarButtonItem:sender animated:YES];
}

This method creates and displays an action sheet asking the user to choose between a GET and POST request. Add the following method at the end of the class implementation (right before @end) to implement the action sheet delegate method:

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (buttonIndex == [actionSheet cancelButtonIndex]) {
        // User pressed cancel -- abort
        return;
    }
    
    // 1
    NSURL *baseURL = [NSURL URLWithString:BaseURLString];
    NSDictionary *parameters = @{@"format": @"json"};
    
    // 2
    AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL];
    manager.responseSerializer = [AFJSONResponseSerializer serializer];
    
    // 3
    if (buttonIndex == 0) {
        [manager GET:@"weather.php" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
            self.weather = responseObject;
            self.title = @"HTTP GET";
            [self.tableView reloadData];
        } failure:^(NSURLSessionDataTask *task, NSError *error) {
            UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                                message:[error localizedDescription]
                                                               delegate:nil
                                                      cancelButtonTitle:@"Ok"
                                                      otherButtonTitles:nil];
            [alertView show];
        }];
    }
    
    // 4
    else if (buttonIndex == 1) {
        [manager POST:@"weather.php" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
            self.weather = responseObject;
            self.title = @"HTTP POST";
            [self.tableView reloadData];
        } failure:^(NSURLSessionDataTask *task, NSError *error) {
            UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                                message:[error localizedDescription]
                                                               delegate:nil
                                                      cancelButtonTitle:@"Ok"
                                                      otherButtonTitles:nil];
            [alertView show];
        }];
    }
}

Here’s what’s happening above:

  1. You first set up the baseURL and the dictionary of parameters.
  2. You then create an instance of AFHTTPSessionManager and set its responseSerializer to the default JSON serializer, similar to the previous JSON example.
  3. If the user presses the button index for HTTP GET, you call the GET method on the manager, passing in the parameters and usual pair of success and failure blocks.
  4. You do the same with the POST version.

In this example you’re requesting JSON responses, but you can easily request either of the other two formats as discussed previously.

Build and run your project, tap on the Client button and then tap on either the HTTP GET or HTTP POST button to initiate the associated request. You should see these screens:

At this point, you know the basics of using AFHTTPSessionManager, but there’s an even better way to use it that will result in cleaner code, which you’ll learn about next.

World Weather Online

Before you can use the live service, you’ll first need to register for a free account on World Weather Online. Don’t worry – it’s quick and easy to do!

After you’ve registered, you should receive a confirmation email at the address you provided, which will have a link to confirm your email address (required). You then need to request a free API key via the My Account page. Go ahead and leave the page open with your API key as you’ll need it soon.

Now that you’ve got your API key, back to AFNetworking…

Hooking into the Live Service

So far you’ve been creating AFHTTPRequestOperation and AFHTTPSessionManager directly from the table view controller as you needed them. More often than not, your networking requests will be associated with a single web service or API.

AFHTTPSessionManager has everything you need to talk to a web API. It will decouple your networking communications code from the rest of your code, and make your networking communications code reusable throughout your project.

Here are two guidelines on AFHTTPSessionManager best practices:

  1. Create a subclass for each web service. For example, if you’re writing a social network aggregator, you might want one subclass for Twitter, one for Facebook, another for Instragram and so on.
  2. In each AFHTTPSessionManager subclass, create a class method that returns a shared singleton instance. This saves resources and eliminates the need to allocate and spin up new objects.

Your project currently doesn’t have a subclass of AFHTTPSessionManager; it just creates one directly. Let’s fix that.

To begin, create a new file in your project of type iOS\Cocoa Touch\Objective-C Class. Call it WeatherHTTPClient and make it a subclass of AFHTTPSessionManager.

You want the class to do three things: perform HTTP requests, call back to a delegate when the new weather data is available, and use the user’s physical location to get accurate weather.

Replace the contents of WeatherHTTPClient.h with the following:

#import "AFHTTPSessionManager.h"

@protocol WeatherHTTPClientDelegate;

@interface WeatherHTTPClient : AFHTTPSessionManager
@property (nonatomic, weak) id<WeatherHTTPClientDelegate>delegate;

+ (WeatherHTTPClient *)sharedWeatherHTTPClient;
- (instancetype)initWithBaseURL:(NSURL *)url;
- (void)updateWeatherAtLocation:(CLLocation *)location forNumberOfDays:(NSUInteger)number;

@end

@protocol WeatherHTTPClientDelegate <NSObject>
@optional
-(void)weatherHTTPClient:(WeatherHTTPClient *)client didUpdateWithWeather:(id)weather;
-(void)weatherHTTPClient:(WeatherHTTPClient *)client didFailWithError:(NSError *)error;
@end

You’ll learn more about each of these methods as you implement them. Switch over to WeatherHTTPClient.m and add the following right after the import statement:

// Set this to your World Weather Online API Key
static NSString * const WorldWeatherOnlineAPIKey = @"PASTE YOUR API KEY HERE";

static NSString * const WorldWeatherOnlineURLString = @"http://api.worldweatheronline.com/free/v1/";

Make sure you replace @”PASTE YOUR KEY HERE” with your actual World Weather Online API Key.

Next paste these methods just after the @implementation line:

+ (WeatherHTTPClient *)sharedWeatherHTTPClient
{
    static WeatherHTTPClient *_sharedWeatherHTTPClient = nil;
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedWeatherHTTPClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:WorldWeatherOnlineURLString]];
    });
    
    return _sharedWeatherHTTPClient;
}

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

    if (self) {
        self.responseSerializer = [AFJSONResponseSerializer serializer];
        self.requestSerializer = [AFJSONRequestSerializer serializer];
    }
    
    return self;
}

The sharedWeatherHTTPClient method uses Grand Central Dispatch to ensure the shared singleton object is only allocated once. You initialize the object with a base URL and set it up to request and expect JSON responses from the web service.

Paste the following method underneath the previous ones:

- (void)updateWeatherAtLocation:(CLLocation *)location forNumberOfDays:(NSUInteger)number
{
    NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
    
    parameters[@"num_of_days"] = @(number);
    parameters[@"q"] = [NSString stringWithFormat:@"%f,%f",location.coordinate.latitude,location.coordinate.longitude];
    parameters[@"format"] = @"json";
    parameters[@"key"] = WorldWeatherOnlineAPIKey;
    
    [self GET:@"weather.ashx" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
        if ([self.delegate respondsToSelector:@selector(weatherHTTPClient:didUpdateWithWeather:)]) {
            [self.delegate weatherHTTPClient:self didUpdateWithWeather:responseObject];
        }
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        if ([self.delegate respondsToSelector:@selector(weatherHTTPClient:didFailWithError:)]) {
            [self.delegate weatherHTTPClient:self didFailWithError:error];
        }
    }];
}

This method calls out to World Weather Online to get the weather for a particular location.

Once the object has loaded the weather data, it needs some way to communicate that data back to whoever’s interested. Thanks to the WeatherHTTPClientDelegate protocol and its delegate methods, the success and failure blocks in the above code can notify a controller that the weather has been updated for a given location. That way, the controller can update what it is displaying.

Now it’s time to put the final pieces together! The WeatherHTTPClient is expecting a location and has a defined delegate protocol, so you need to update the WTTableViewController class to take advantage of this.

Open up WTTableViewController.h to add an import and replace the @interface declaration as follows:

#import "WeatherHTTPClient.h"

@interface WTTableViewController : UITableViewController <NSXMLParserDelegate, CLLocationManagerDelegate, UIActionSheetDelegate, WeatherHTTPClientDelegate>

Also add a new Core Location manager property:

@property (nonatomic, strong) CLLocationManager *locationManager;

In WTTableViewController.m, add the following lines to the bottom of viewDidLoad::

self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;

These lines initialize the Core Location manager to determine the user’s location when the view loads. The Core Location manager then reports that location via a delegate callback. Add the following method to the implementation:

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    // Last object contains the most recent location
    CLLocation *newLocation = [locations lastObject];
    
    // If the location is more than 5 minutes old, ignore it
    if([newLocation.timestamp timeIntervalSinceNow] > 300)
        return;
    
    [self.locationManager stopUpdatingLocation];

    WeatherHTTPClient *client = [WeatherHTTPClient sharedWeatherHTTPClient];
    client.delegate = self;
    [client updateWeatherAtLocation:newLocation forNumberOfDays:5];
}

Now when there’s an update to the user’s whereabouts, you can call the singleton WeatherHTTPClient instance to request the weather for the current location.

Remember, WeatherHTTPClient has two delegate methods itself that you need to implement. Add the following two methods to the implementation:

- (void)weatherHTTPClient:(WeatherHTTPClient *)client didUpdateWithWeather:(id)weather
{
    self.weather = weather;
    self.title = @"API Updated";
    [self.tableView reloadData];
}

- (void)weatherHTTPClient:(WeatherHTTPClient *)client didFailWithError:(NSError *)error
{
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                        message:[NSString stringWithFormat:@"%@",error]
                                                       delegate:nil
                                              cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [alertView show];
}

When the WeatherHTTPClient succeeds, you update the weather data and reload the table view. In case of a network error, you display an error message.

Find the apiTapped: method and replace it with the following:

- (IBAction)apiTapped:(id)sender
{
    [self.locationManager startUpdatingLocation];
}

Build and run your project (try your device if you have any troubles with your simulator), tap on the API button to initiate the WeatherHTTPClient request, and you should see something like this:

Here’s hoping your upcoming weather is as sunny as mine!