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 2 of 4 of this article. Click here to view the first page.

Operation Property Lists

Property lists (or plists for short) are just XML files structured in a certain way (defined by Apple). Apple uses them all over the place for things like storing user settings. They look something like this:


<dict>
  <key>data</key>
  <dict>
    <key>current_condition</key>
      <array>
      <dict>
        <key>cloudcover</key>
        <string>16</string>
        <key>humidity</key>
        <string>59</string>
        ...

The above represents:

  • A dictionary with a single key called “data” that contains another dictionary.
  • That dictionary has a single key called “current_condition” that contains an array.
  • That array contains a dictionary with several keys and values, like cloudcover=16 and humidity=59.

It’s time to load the plist version of the weather data. Find the plistTapped: method and replace the empty implementation with the following:

- (IBAction)plistTapped:(id)sender 
{
    NSString *string = [NSString stringWithFormat:@"%@weather.php?format=plist", BaseURLString];
    NSURL *url = [NSURL URLWithString:string];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
    
    // Make sure to set the responseSerializer correctly
    operation.responseSerializer = [AFPropertyListResponseSerializer serializer];
    
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        
        self.weather = (NSDictionary *)responseObject;
        self.title = @"PLIST Retrieved";
        [self.tableView reloadData];
        
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
      
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                            message:[error localizedDescription]
                                                           delegate:nil
                                                  cancelButtonTitle:@"Ok"
                                                  otherButtonTitles:nil];
        [alertView show];
    }];
    
    [operation start];
}

Notice that this code is almost identical to the JSON version, except for changing the responseSerializer to the default AFPropertyListResponseSerializer to let AFNetworking know that you’re going to be parsing a plist.

That’s pretty neat: your app can accept either JSON or plist formats with just a tiny change to the code!

Build and run your project and try tapping on the PLIST button. You should see something like this:

The Clear button in the top navigation bar will clear the title and table view data so you can reset everything to make sure the requests are going through.

Operation XML

While AFNetworking handles JSON and plist parsing for you, working with XML is a little more complicated. This time, it’s your job to construct the weather dictionary from the XML feed.

Fortunately, iOS provides some help via the NSXMLParser class (which is a SAX parser, if you want to read up on it).

Still in WTTableViewController.m, find the xmlTapped: method and replace its implementation with the following:

- (IBAction)xmlTapped:(id)sender
{
    NSString *string = [NSString stringWithFormat:@"%@weather.php?format=xml", BaseURLString];
    NSURL *url = [NSURL URLWithString:string];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];

    // Make sure to set the responseSerializer correctly
    operation.responseSerializer = [AFXMLParserResponseSerializer serializer];
    
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        
        NSXMLParser *XMLParser = (NSXMLParser *)responseObject;
        [XMLParser setShouldProcessNamespaces:YES];
        
        // Leave these commented for now (you first need to add the delegate methods)
        // XMLParser.delegate = self;
        // [XMLParser parse];
        
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        
        UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                            message:[error localizedDescription]
                                                           delegate:nil
                                                  cancelButtonTitle:@"Ok"
                                                  otherButtonTitles:nil];
        [alertView show];
        
    }];
    
    [operation start];
}

This should look pretty familiar by now. The biggest change is that in the success block you don’t get a nice, preprocessed NSDictionary object passed to you. Instead, responseObject is an instance of NSXMLParser, which you will use to do the heavy lifting in parsing the XML.

You’ll need to implement a set of delegate methods for NXMLParser to be able to parse the XML. Notice that XMLParser’s delegate is set to self, so you will need to add NSXMLParser’s delegate methods to WTTableViewController to handle the parsing.

First, update WTTableViewController.h and change the class declaration at the top as follows:

@interface WTTableViewController : UITableViewController<NSXMLParserDelegate>

This means the class will implement the NSXMLParserDelegate protocol. You will implement these methods soon, but first you need to add a few properties.

Add the following properties to WTTableViewController.m within the class extension, right after @interface WTTableViewController () :

@property(nonatomic, strong) NSMutableDictionary *currentDictionary;   // current section being parsed
@property(nonatomic, strong) NSMutableDictionary *xmlWeather;          // completed parsed xml response
@property(nonatomic, strong) NSString *elementName;
@property(nonatomic, strong) NSMutableString *outstring;

These properties will come in handy when you’re parsing the XML.

Now paste this method in WTTableViewController.m, right before @end:

- (void)parserDidStartDocument:(NSXMLParser *)parser
{
    self.xmlWeather = [NSMutableDictionary dictionary];
}

The parser calls this method when it first starts parsing. When this happens, you set self.xmlWeather to a new dictionary, which will hold hold the XML data.

Next paste this method right after this previous one:

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
    self.elementName = qName;
    
    if([qName isEqualToString:@"current_condition"] ||
       [qName isEqualToString:@"weather"] ||
       [qName isEqualToString:@"request"]) {
        self.currentDictionary = [NSMutableDictionary dictionary];
    }
    
    self.outstring = [NSMutableString string];
}

The parser calls this method when it finds a new element start tag. When this happens, you keep track of the new element’s name as self.elementName and then set self.currentDictionary to a new dictionary if the element name represents the start of a new weather forecast. You also reset outstring as a new mutable string in preparation for new XML to be received related to the element.

Next paste this method just after the previous one:

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
    if (!self.elementName)
        return;
    
    [self.outstring appendFormat:@"%@", string];
}

As the name suggests, the parser calls this method when it finds new characters on an XML element. You append the new characters to outstring, so they can be processed once the XML tag is closed.

Again, paste this next method just after the previous one:

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
{
    // 1
    if ([qName isEqualToString:@"current_condition"] ||
       [qName isEqualToString:@"request"]) {
        self.xmlWeather[qName] = @[self.currentDictionary];
        self.currentDictionary = nil;
    }
    // 2
    else if ([qName isEqualToString:@"weather"]) {
        
        // Initialize the list of weather items if it doesn't exist
        NSMutableArray *array = self.xmlWeather[@"weather"] ?: [NSMutableArray array];
        
        // Add the current weather object
        [array addObject:self.currentDictionary];
        
        // Set the new array to the "weather" key on xmlWeather dictionary
        self.xmlWeather[@"weather"] = array;
        
        self.currentDictionary = nil;
    }
    // 3
    else if ([qName isEqualToString:@"value"]) {
        // Ignore value tags, they only appear in the two conditions below
    }
    // 4
    else if ([qName isEqualToString:@"weatherDesc"] ||
            [qName isEqualToString:@"weatherIconUrl"]) {
        NSDictionary *dictionary = @{@"value": self.outstring};
        NSArray *array = @[dictionary];
        self.currentDictionary[qName] = array;
    }
    // 5
    else if (qName) {
        self.currentDictionary[qName] = self.outstring;
    }
    
	self.elementName = nil;
}

This method is called when an end element tag is encountered. When that happens, you check for a few special tags:

  1. The current_condition element indicates you have the weather for the current day. You add this directly to the xmlWeather dictionary.
  2. The weather element means you have the weather for a subsequent day. While there is only one current day, there may be several subsequent days, so you add this weather information to an array.
  3. The value tag only appears inside other tags, so it’s safe to skip over it.
  4. The weatherDesc and weatherIconUrl element values need to be boxed inside an array before they can be stored. This way, they will match how the JSON and plist versions of the data are structured exactly.
  5. All other elements can be stored as is.

Now for the final delegate method! Paste this method just after the previous one:

- (void) parserDidEndDocument:(NSXMLParser *)parser
{
    self.weather = @{@"data": self.xmlWeather};
    self.title = @"XML Retrieved";
    [self.tableView reloadData];
}

The parser calls this method when it reaches the end of the document. At this point, the xmlWeather dictionary that you’ve been building is complete, so the table view can be reloaded.

Wrapping xmlWeather inside another NSDictionary might seem redundant, but this ensures the format matches up exactly with the JSON and plist versions. This way, all three data formats can be displayed with the same code!

Now that the delegate methods and properties are in place, return to the xmlTapped: method and uncomment the lines of code from before:

- (IBAction)xmlTapped:(id)sender
{
    ...
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        
        NSXMLParser *XMLParser = (NSXMLParser *)responseObject;
        [XMLParser setShouldProcessNamespaces:YES];
        
        // These lines below were previously commented
        XMLParser.delegate = self;
        [XMLParser parse];    
    ...
}

Build and run your project. Try tapping the XML button, and you should see this: