Thomas Denney

Working with AFNetworking 2

This week Mattt Thompson announced AFNetworking 2.0 and for the past couple of days I’ve been playing around with it (along with a few techniques from objc.io for lighter view controllers) to build a simple demo app that showcases some of the new features of AFNetworking. The source for the app discussed in this post is available on my GitHub profile. To compile the code in this blog post you will need Xcode 5 and the iOS 7 SDK installed.

Getting started

With CocoaPods it was really easy to set the project up, however slightly different from the original AFNetworking. Here is my Podfile:

platform :ios, '7.0'
pod "AFNetworking", "2.0.0"
pod "AFNetworking/UIKit+AFNetworking", "2.0.0

The UIKit extensions for AFNetworking previously only extended UIImageView, however this has now been greatly expanded for other UIViews so is now a separate subspec in CocoaPods. Simply run ‘pod install’ and open up your Xcode Workspace (not the project) to get started.

Basic serializers

AFNetworking 2 has moved to a new model of serializers to add a bit more modularity. This means that requests can now have a request serializer that handles generating the request based on user parameters and doing tasks such as authorization and the response serializer can now generate user data from the server response.

In my example app I’ve written a simple response serializer that subclasses AFJSONSerializer to serialize an array from the JSON response from reddit:

@implementation RedditResponseSerializer

-(id)responseObjectForResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *__autoreleasing *)error
{
    NSDictionary * json = [super responseObjectForResponse:response data:data error:error];
    NSMutableArray * posts = [NSMutableArray new];
    NSDictionary * dataObject = json[@"data"];
    if (dataObject != nil)
    {
        NSArray * children = dataObject[@"children"];
        [children enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            NSDictionary * postData = [(NSDictionary*)obj objectForKey:@"data"];
            [posts addObject:[RedditPost postWithProperties:postData]];
        }];
    }
    return posts;
}

@end

The great thing about writing a custom serializer is that it is highly reusable - it isn’t going to take much work in this example to check whether the response was acutally for comments instead of posts, so it can simply return another array of objects. I’ve also written a simple class for storing post data:

@implementation RedditPost

+(id)postWithProperties:(NSDictionary *)properties
{
    return [[self alloc] initWithProperties:properties];
}

-(id)initWithProperties:(NSDictionary *)properties
{
    self = [super init];
    if (self) {
        [self setValuesForKeysWithDictionary:properties];
    }
    return self;
}

//Had to do a custom implementation of this because I didn't want all keys
-(void)setValue:(id)value forKey:(NSString *)key
{
    if ([key isEqualToString:@"title"]) self.title = value;
    else if ([key isEqualToString:@"subreddit"]) self.subreddit = value;
    else if ([key isEqualToString:@"author"]) self.username = value;
    else if ([key isEqualToString:@"thumbnail"]) self.thumbnail = value;
    else if ([key isEqualToString:@"url"]) self.url = value;
    else if ([key isEqualToString:@"ups"]) self.ups = value;
    else if ([key isEqualToString:@"downs"]) self.downs = value;
    else if ([key isEqualToString:@"score"]) self.score = value;
}

@end

As noted in the comment above, if you do not wish to have properties for all of the possible API keys it makes sense to write a custom setValue:forKey: function to only use the keys you want. This is especially sensible if in the future new keys are added to the API as otherwise your app will crash with an NSUndefinedKeyException.

Then, using the light view controller pattern described in objc.io #1, it is really easy to set up a data source for the app:

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://reddit.com/.json"]];
    AFHTTPRequestOperation * operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
    operation.responseSerializer = [RedditResponseSerializer serializer];
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSArray * allPosts = (NSArray*)responseObject;
        [self configureDataSource:allPosts];
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"%@", error.localizedDescription);
    }];
    [operation start];
}

-(void)configureDataSource:(NSArray*)posts
{
    self.dataSource = [[ArrayDataSource alloc] initWithItems:posts cellIdentifier:@"redditPostCell" configureCellBlock:^(id cell, id item) {
        RedditPost * post = (RedditPost*)item;
        UITableViewCell * postCell = (UITableViewCell*)cell;
        postCell.textLabel.text = post.title;
        postCell.detailTextLabel.text = [NSString stringWithFormat:@"%d points \u2022 /r/%@ \u2022 /u/%@", post.score.integerValue, post.subreddit, post.username];

    }];
    self.tableView.dataSource = self.dataSource;
    [self.tableView reloadData];
}

In this example I’ve simply requested the JSON for the front page of reddit, used my custom serializer to get an array of the posts and then configured a simple data source for the posts. It is worth noting that you must retain your ArrayDataSource object (i.e. you can’t define it in configureDataSource:) because the dataSource property of a UITableView expects only a weak reference to an id, so I instead added it as a property.

More advanced serializer

This example is similiar to the last, however adopts a few new features:

  • Use a custom AFHTTPSessionManager which makes it easy to perform custom requests. In this example I’ll show a simple way to get stock market quotes from Yahoo Finance
  • Use a custom request serializer to set up the URL request based on an array of stock symbols
  • Use a custom response serializer and YahooStockValue object to parse the CSV data from Yahoo
  • Return the data to the main view controller so it can be displayed in a list

Custom AFHTTPSessionManager

AFHTTPSessionManager is the new version of AFHTTPClient, and subclasses AFURLSessionManager which implements methods of the new NSURLSession (introduced in iOS 7). Clients/Session managers are recommended to return a singleton instance, as below:

//YahooFinanceClient.h
#import <Foundation/Foundation.h>
#import <AFNetworking.h>
#import "YahooFinanceRequestSerializer.h"
#import "YahooFinanceResponseSerializer.h"

typedef void(^FetchedSymbols)(NSArray*symbols);

@interface YahooFinanceClient : AFHTTPSessionManager

+(instancetype)client;

-(void)fetchSymbols:(NSArray*)symbols completion:(FetchedSymbols)fetchedSymbolsBlock;

@end

//YahooFinanceClient.m
#import "YahooFinanceClient.h"

@implementation YahooFinanceClient

#pragma mark - Singleton

+(instancetype)client
{
    static YahooFinanceClient * requests = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        requests = [[YahooFinanceClient alloc] initWithBaseURL:[NSURL URLWithString:@"http://download.finance.yahoo.com/d/quotes.csv"]];
    });
    return requests;
}

#pragma mark - custom initialization

-(id)initWithBaseURL:(NSURL *)url
{
    self = [super initWithBaseURL:url];
    if (self)
    {
        self.requestSerializer = [YahooFinanceRequestSerializer new];
        self.responseSerializer = [YahooFinanceResponseSerializer new];
    }
    return self;
}

#pragma mark - Custom fetching functions

-(void)fetchSymbols:(NSArray *)symbols completion:(FetchedSymbols)fetchedSymbolsBlock
{
    [self GET:@"" parameters:@{@"symbols": symbols} success:^(NSURLSessionDataTask *task, id responseObject) {
        fetchedSymbolsBlock(responseObject);
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
        fetchedSymbolsBlock([NSArray new]);
        NSLog(@"ERROR: %@", error);
    }];
}

@end

This sets up a client with a base URL of the API endpoint we will be using. Because this is only fetching data from one endpoint, we will use that one. If we were requesting from Twitter, for example, we would use https://api.twitter.com/1.1/ and then when we wanted to perform a request rather than using a URL of @"" we would use the appropriate API endpoint (i.e. @“statuses/mentions_timeline.json”).

I won’t include all of the source code for the request serializer here as it is relatively simple, so please view the source on GitHub.

Custom response serializer

There are two components to the response serializer: the first is the serializer that returns an array of all of the stock symbols and the second is some code in YahooStockValue that will receive an array of the parsed CSV values that it can then build an object from:

//YahooFinanceResponseSerializer.m implementation

-(id)responseObjectForResponse:(NSURLResponse *)response data:(NSData *)data error:(NSError *__autoreleasing *)error
{
    NSString * strData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

    NSMutableArray * stockValues = [NSMutableArray new];

    [strData enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
        NSArray * components = [self parseLine:line];
        YahooStockValue * stockValue = [[YahooStockValue alloc] initWithArray:components];
        [stockValues addObject:stockValue];
    }];

    return stockValues;
}

-(NSArray*)parseLine:(NSString*)line
{
    NSMutableArray * allComponents = [NSMutableArray new];
    NSMutableString * currentComponent = [NSMutableString new];
    BOOL inQuotes = NO;
    BOOL lastWasBackslash = NO;
    for (NSInteger i = 0; i < line.length; i++)
    {
        unichar chr = [line characterAtIndex:i];
        if (chr == '"' && !lastWasBackslash) inQuotes = !inQuotes;
        else if (chr == '\\') lastWasBackslash = YES;
        else if (!inQuotes && chr == ',')
        {
            [allComponents addObject:currentComponent];
            currentComponent = [NSMutableString new];
        }
        else
        {
            lastWasBackslash = NO;
            [currentComponent appendFormat:@"%c", chr];
        }
    }

    if (currentComponent.length > 0) [allComponents addObject:currentComponent];

    return allComponents;
}

//YahooStockValue.m implementation
-(id)initWithArray:(NSArray *)array
{
    self = [super init];
    if (self)
    {
        NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
        [f setNumberStyle:NSNumberFormatterDecimalStyle];

        if (array.count >= 1) self.name = array[0];
        if (array.count >= 2) self.symbol = array[1];
        if (array.count >= 3) self.latestValue = [f numberFromString:array[2]];
        if (array.count >= 4) self.openValue = [f numberFromString:array[3]];
        if (array.count >= 5) self.closeValue = [f numberFromString:array[4]];
    }
    return self;
}   

Hopefully you will find the majority of this code reasonably familiar and I won’t focus on the CSV parser itself. In the example app you can type in the stock symbols for various companies separated by spaces and the data will be fetched, parsed and presented in a table view.

Image loading

AFNetworking has always had a really awesome extension to UIImageView that allows you to easily load images from the web, however there are now a few more additions so they a now in a separate CocoaPod subspec (as mentioned in the introduction). Simply do the following to load an image from the web:

#import <UIImageView+AFNetworking.h>

[imageView setImageWithURL:[NSURL URLWithString:@"http://programmingthomas.com/item/52356701e4b0bfcfa19b4b3b?format=1500w"]];

There are a few more methods on the category that also allow you to setup placeholder images and use callbacks for success/failure. It is particularly useful to use this category for all image loading from the web because it also manages caching for you as well with a singleton cache, so you don’t need to worry about requesting images multiple times.

Notes on migrating to and using AFNetworking 2

Whilst AFNetworking 2 is similiar to the original version it introduces a lot of new features that break compatibility, so there are a few things to keep note of:

  • You will need to target iOS 7 upwards because of the migration towards NSURLSession
  • AFHTTPClient has now been been replaced with various classes inheriting from AFURLSessionManager instead (including AFHTTPSessionManager)
  • When working with web APIs:
    • Your session manager should provide convenience methods for getting data from an API endpoint. It should also provide a singleton method so you do not recreate it unnecessarily
    • Your session manager should use custom request and response serializers
    • Your request serializer will convert the options in your application into parameters to add to the URL. It should also add and handle authentication parameters. In some cases, if you do not need to handle authentication or complex parameters, you may be able to use one of the standard AFNetworking request serializers instead
    • Your response serializer should convert the NSData response from the server into something more useful for your application to use such as an array of/or custom objects. If you are handling XML or JSON data you probably want to base it off of AFXMLResponseSerializer or AFJSONResponseSerializer as these will handle parsing for you

Conclusion

I haven’t covered all of the new features (including Rocket support for real-time events from the server) of AFNetworking 2 but the changes have helped to make it even easier to write applications on top of it.

At first I did feel that the model for session manager + request serializer + response serializer was a little verbose, however it makes perfect sense:

One of the major criticisms of AFNetworking is how bulky it is. Although its architecture lent itself well to modularity on a class level, its packaging didn’t allow for individual features to be selected à la carte. Over time, AFHTTPClient in particular became overburdened in its responsibilities (creating requests, serializing query string parameters, determining response parsing behavior, creating and managing operations, monitoring network reachability).

http://nshipster.com/afnetworking-2/

This new modularity means that the amount of code in the session manager (formally the HTTP client) is a much smaller because tasks are delegated out into more appropriate areas, and whilst my example may have been a bit too small to demonstrate this fully, it makes perfect sense if you have a much larger API to deal with.

AFNetworking 2 is really awesome, and hopefully you’ll be able to begin using it in your apps soon.