Saturday, April 13, 2013

AFNetworking Crash Course http://www.raywenderlich.com/30445/afnetworking-crash-course

http://www.raywenderlich.com/30445/afnetworking-crash-course

AFNetworking Crash Course

18 MARCH 2013

This post is also available in: Chinese (Simplified)

If you're new here, you may want to subscribe to my RSS feed or follow me on Twitter. Thanks for visiting!

Learn how to use AFNetworking: an easy to use network API for iOS!

Learn how to use AFNetworking: an easy to use network API for iOS!

This is a post by Tutorial Team Member Scott Sherwood, co-founder of Dynamically Loaded, a location-based software company specializing in hybrid-positioning solutions.

Networking – your app can't live without it! Apple's own NSURLConnection in the Foundation framework can be difficult to understand, but there's an easy-to-use alternative: AFNetworking.

AFNetworking is also incredibly popular – it won our Reader's Choice 2012 Best iOS Library Award. So it's time we wrote a tutorial to show you how to effectively use it in your apps.

AFNetworking contains everything you need to interface with online resources, from web services to file downloads. It also helps you ensure that your UI is responsive even when your app is in the middle of a big download.

This tutorial will take you through the major components of the AFNeworking framework. Along the way, you'll build a Weather App that uses feeds from World Weather Online. You'll start with static weather data, but by the end of the tutorial, the app will be fully connected to live weather feeds.

Today's forecast: a cool developer learns all about AFNetworking and gets inspired to use it in his/her apps. Let's get busy!

Getting Started

First download the starter project for this tutorial here. This project provides a basic UI to get you started – no AFNetworking code has been added yet.

Open MainStoryboard.storyboard and you will see three view controllers:



From left to right, they are:

  • A top-level navigation controller;
  • A table view controller that will display the weather, one row per day;
  • A custom view controller (WeatherAnimationViewController) that will show the weather for a single day when the user taps on a table view cell.

Build and run the project, and you'll see the UI appear, but nothing works yet! That's because the app needs to get its data from the network, and that code hasn't been added yet. That is what you will be doing in this tutorial!

The first thing you need to do is include the AFNetworking framework in your project. Download the latest version from GitHub if you don't have it already.

When you unzip the file, you will see that it has an AFNetworking subfolder with all of the .h and .m files, as highlighted below:



Drag that inner AFNetworking into your Xcode project.



When presented with the options for adding the folder, make sure that Copy items into destination group's folder (if needed) and Create groups for any added folders are both checked.

To complete the setup, open the pre-compiled header Weather-Prefix.pch from the Supporting Filessection of the project. Add this line after the other imports:

  #import "AFNetworking.h"

Adding AFNetworking to the pre-compiled header means that the framework will be automatically included in all the project's source files.

Pretty easy, eh? Now you're ready to "weather" the code!

Operation JSON

AFNetworking is smart enough to load and process structured data over the network, as well as plain old HTTP requests. In particular, it supports JSON, XML and Property Lists (plists).

You could download some JSON and then run it through a parser yourself, but why bother? AFNetworking can do it all!

First you need the base URL of your test script. Add this static NSString declaration to the top ofWTTableViewController.m, just underneath all the #import lines:

This is the URL to an incredibly simple "web service" that I created for you for this tutorial. If you're curious what it looks like, you can download the source.

The web service returns weather data in three different formats – JSON, XML, and PLIST. You can take a look at the data it can return by using these URLS:

The first data format you will be using is JSON. JSON is a very common JavaScript-derived object format. It looks something like this:

  {      "data": {          "current_condition": [              {                  "cloudcover": "16",                  "humidity": "59",                  "observation_time": "09:09 PM",              }          ]      }  }

Note: If you'd like to learn more about JSON, check out our Working with JSON in iOS 5 Tutorial.

When the user taps the JSON button in the app, you want to load and process some JSON data from the server. In WTTableViewController.m, find the jsonTapped: method (it should be empty) and replace it with the following:

  - (IBAction)jsonTapped:(id)sender {      // 1      NSString *weatherUrl = [NSString stringWithFormat:@"%@weather.php?format=json", BaseURLString];      NSURL *url = [NSURL URLWithString:weatherUrl];      NSURLRequest *request = [NSURLRequest requestWithURL:url];         // 2      AFJSONRequestOperation *operation =      [AFJSONRequestOperation JSONRequestOperationWithRequest:request          // 3          success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {              self.weather  = (NSDictionary *)JSON;              self.title = @"JSON Retrieved";              [self.tableView reloadData];          }          // 4          failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {              UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"                                                           message:[NSString stringWithFormat:@"%@",error]                                                          delegate:nil                                                 cancelButtonTitle:@"OK" otherButtonTitles:nil];              [av show];          }];         // 5      [operation start];  }

This is your first AFNetworking code! Since this is all new, I'll explain this method one section at a time.

  1. You form the full URL from the base URL. The full URL then makes its way into an NSURL object, and then into an NSURLRequest.
  2. AFJSONRequestOperation is the all-in-one class that fetches data across the network and then parses the JSON response.
  3. The success block runs when (surprise!) the request succeeds. In this example, the parsed weather data comes back as a dictionary in the JSON variable, which is stored in the weather property.
  4. The failure block runs if something goes wrong, such as when the network isn't available. If that happens, you display an alert view with an error message.

As you can see, AFNetworking is extremely simple to use. This same task (downloading and parsing JSON data) would have taken many lines of code with raw Apple APIs like NSURLConnection.

Now that the weather data is safely in self.weather, you need to display it. Find thetableView:numberOfRowsInSection: method and replace it with the following:

  - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section  {      // Return the number of rows in the section.         if(!self.weather)          return 0;         switch (section) {          case 0:{              return 1;          }          case 1:{              NSArray *upcomingWeather = [self.weather upcomingWeather];              return [upcomingWeather count];          }          default:              return 0;      }  }

The table view will have two sections: the first to display the current weather and the second to display the upcoming weather.

Wait a minute, you might be thinking. What is this [self.weather upcomingWeather]? If self.weather is a plain old NSDictionary, how does it know what "upcomingWeather" is?

To make it easier to parse the data, there are a couple of helper NSDictionary categories in the starter project:

  • NSDictionary+weather.m
  • NSDictionary+weather_package.m

These categories add some handy methods that make it a little easier to access the data element. You want to focus on the networking part and not on navigating NSDictionary keys, right?

Back in WTTableViewController.m, find the tableView:cellForRowAtIndexPath: method and replace it with the following implementation:

  - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  {      static NSString *CellIdentifier = @"WeatherCell";      UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];      NSDictionary *daysWeather;         switch (indexPath.section) {          case 0:{              daysWeather = [self.weather currentCondition];              break;          }          case 1:{              NSArray *upcomingWeather = [self.weather upcomingWeather];              daysWeather = [upcomingWeather objectAtIndex:indexPath.row];          }          default:              break;      }         cell.textLabel.text = [daysWeather weatherDescription];         // maybe some code will be added here later...         return cell;  }

Like the tableView:numberOfRowsInSection: method, the handy NSDictionary categories are used to get at the data. The current day's weather is a dictionary, and the upcoming days are stored in an array.

Build and run your project, and tap on the JSON button. That will get the AFJSONOperation object in motion, and you should see this:



JSON success!

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 *weatherUrl = [NSString stringWithFormat:@"%@weather.php?format=plist",BaseURLString];      NSURL *url = [NSURL URLWithString:weatherUrl];      NSURLRequest *request = [NSURLRequest requestWithURL:url];         AFPropertyListRequestOperation *operation =      [AFPropertyListRequestOperation propertyListRequestOperationWithRequest:request          success:^(NSURLRequest *request, NSHTTPURLResponse *response, id propertyList) {              self.weather  = (NSDictionary *)propertyList;              self.title = @"PLIST Retrieved";              [self.tableView reloadData];          }          failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id propertyList) {              UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"                                message:[NSString stringWithFormat:@"%@",error]                                                          delegate:nil                                                 cancelButtonTitle:@"OK"                                                 otherButtonTitles:nil];              [av show];      }];         [operation start];  }

Notice that this code is almost identical to the JSON version, except for the change of operation type from AFJSONOperation to AFPropertyListOperation. 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 if you need to reset everything to make sure the requests are going through.

Operation XML

While AFNetworking handles JSON and plist parsing in a similar way and without much effort, working with XML is a little more complicated. This time, it's your job to construct the weather NSDictionary from the XML feed.

iOS provides a little help with the NSXMLParser class (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 *weatherUrl = [NSString stringWithFormat:@"%@weather.php?format=xml",BaseURLString];      NSURL *url = [NSURL URLWithString:weatherUrl];      NSURLRequest *request = [NSURLRequest requestWithURL:url];         AFXMLRequestOperation *operation =      [AFXMLRequestOperation XMLParserRequestOperationWithRequest:request          success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSXMLParser *XMLParser) {              //self.xmlWeather = [NSMutableDictionary dictionary];              XMLParser.delegate = self;              [XMLParser setShouldProcessNamespaces:YES];              [XMLParser parse];          }          failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSXMLParser *XMLParser) {              UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"                                                           message:[NSString stringWithFormat:@"%@",error]                                                          delegate:nil                                                 cancelButtonTitle:@"OK"                                                 otherButtonTitles:nil];              [av show];      }];         [operation start];  }

By now, this should look familiar. The big change is that in the success block, you don't get a nice preprocessed NSDictionary object passed to you. Instead, AFXMLRequestOperation has instantiated a NSXMLParser object that will do some of the heavy lifting of parsing the XML.

The NSXMLParser object has a set of delegate methods you'll need to implement to make sense of the XML. Notice that the XMLParser's delegate is set to self, so the WTTableViewController will handle the XML parsing itself.

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. Next add the following delegate method declarations after the @implementation line:

  - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict;  - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string;  - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName;  -(void) parserDidEndDocument:(NSXMLParser *)parser;

To support the parsing of the feed, you need a few properties to hold some data. Add the following properties after the @implementation line:

  @property(strong) NSMutableDictionary *xmlWeather; //package containing the complete response  @property(strong) NSMutableDictionary *currentDictionary; //current section being parsed  @property(strong) NSString *previousElementName;  @property(strong) NSString *elementName;  @property(strong) NSMutableString *outstring;

Open up WTTableViewController.m so you can consider the delegate methods one-by-one. Paste this method somewhere in the implementation:

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

The NSXMLParser calls this method when it finds a new element start tag. When that happens, you keep track of the previous element name before constructing a new dictionary for that section of the data in the currentDictionary property. You also reset the string outstring that you'll build as you read the XML inside this tag.

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, you call this method when the parser finds character data while inside an XML tag. The method appends this character data to the outstring property, to 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 setObject:[NSArray arrayWithObject:self.currentDictionary] forKey:qName];          self.currentDictionary = nil;      }      // 2      else if([qName isEqualToString:@"weather"]){             // Initalise the list of weather items if it dosnt exist          NSMutableArray *array = [self.xmlWeather objectForKey:@"weather"];          if(!array)              array = [NSMutableArray array];             [array addObject:self.currentDictionary];          [self.xmlWeather setObject:array forKey:@"weather"];             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"]){          [self.currentDictionary setObject:[NSArray arrayWithObject:[NSDictionary dictionaryWithObject:self.outstring forKey:@"value"]] forKey:qName];      }      // 5      else {          [self.currentDictionary setObject:self.outstring forKey:qName];      }     	self.elementName = nil;  }

You call this method when you detect an element end tag. When that happens, you want to look for a few special tags:

  1. The current_condition element means you have the weather for the current day. That can be added 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 are several subsequent days, so the weather here is added to an array.
  3. The value tag 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 are stored to match how the JSON and plist versions are structured.
  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 = [NSDictionary dictionaryWithObject:self.xmlWeather forKey:@"data"];      self.title = @"XML Retrieved";      [self.tableView reloadData];  }

You call this method when the parser reaches the end of the document. By this point, the xmlWeather dictionary you've been building is complete and the table view can be reloaded.

Adding xmlWeather inside an NSDictionary seems redundant, but this ensures the format matches up exactly with the JSON and plist versions. That means all three data formats can be displayed with the same code!

Now that the delegate methods and properties are in place, find the xmlTapped: method and uncomment the one line in the success block:

  -(IBAction)xmlTapped:(id)sender{      ...      success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSXMLParser *XMLParser) {          // the line below used to be commented out          self.xmlWeather = [NSMutableDictionary dictionary];          XMLParser.delegate = self;      ...  }

Build and run your project, and try tapping the XML button. You should see this:



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, 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 finalreturn cell; line (there should be a comment marking the spot):

     __weak UITableViewCell *weakCell = cell;     [cell.imageView setImageWithURLRequest:[[NSURLRequest alloc] initWithURL:[NSURL URLWithString:daysWeather.weatherIconURL]]                        placeholderImage:[UIImage imageNamed:@"placeholder.png"]                                 success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image){                                     weakCell.imageView.image = image;                                        //only required if no placeholder is set to force the imageview on the cell to be laid out to house the new image.                                     //if(weakCell.imageView.frame.size.height==0 || weakCell.imageView.frame.size.width==0 ){                                     [weakCell setNeedsLayout];                                     //}                                 }                                 failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error){                                    }];

First you create a weak reference to the cell for use inside the block. If you access the cell variable directly, Xcode will warn you about a potential retain cycle and memory leak.

UIImageView+AFNetworking makes the setImageWithURLRequest… method available to you. This method takes a request URL to the image, a placeholder image, a success block and a failure block.

When the cell is first created, its UIImageView will display the placeholder image until the real image has finished downloading. However, you need to be sure the placeholder image is the same size as the actual image.

If it isn't the same size, you can call setNeedsLayout on the imageView in the success block. The code above comments this line out because the placeholder image is properly sized, but it may be of use to you in other applications.

Now build and run your project and tap on any of the three operations you've added so far. You should see this:



Nice! Asynchronously loading images has never been easier.

A RESTful Class

So far you've been creating one-off HTTP requests with classes like AFJSONRequestOperation. On the other hand, the lower-level AFHTTPClient class is designed for accessing a single web service endpoint. You set its base URL and then make several requests with that object, rather than creating a client each time like you've done so far.

AFHTTPClient also gives greater flexibility to encode parameters, handles multipart form request body construction, and manages request operations and enqueuing of operations into batches. It handles the full suite of RESTful verbs (GET, POST, PUT, and DELETE), so you'll try two of the most common ones: GET and POST.

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 httpClientTapped: method and replace its implementation with the following:

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

This calls up an action sheet asking the user to choose between a GET and a POST request. Paste in the following method to implement the action sheet button press:

  - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{      // 1      NSURL *baseURL = [NSURL URLWithString:[NSString stringWithFormat:BaseURLString]];      NSDictionary *parameters = [NSDictionary dictionaryWithObject:@"json" forKey:@"format"];         // 2      AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:baseURL];      [client registerHTTPOperationClass:[AFJSONRequestOperation class]];      [client setDefaultHeader:@"Accept" value:@"application/json"];         // 3      if (buttonIndex==0) {          [client postPath:@"weather.php"                parameters:parameters                   success:^(AFHTTPRequestOperation *operation, id responseObject) {                       self.weather = responseObject;                       self.title = @"HTTP POST";                       [self.tableView reloadData];                   }                   failure:^(AFHTTPRequestOperation *operation, NSError *error) {                       UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"                                                                    message:[NSString stringWithFormat:@"%@",error]                                                                   delegate:nil                                                          cancelButtonTitle:@"OK" otherButtonTitles:nil];                       [av show];                      }           ];      }      // 4      else if (buttonIndex==1) {          [client getPath:@"weather.php"               parameters:parameters                  success:^(AFHTTPRequestOperation *operation, id responseObject) {                      self.weather = responseObject;                      self.title = @"HTTP GET";                      [self.tableView reloadData];                  }                  failure:^(AFHTTPRequestOperation *operation, NSError *error) {                      UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"                                                                   message:[NSString stringWithFormat:@"%@",error]                                                                  delegate:nil                                                         cancelButtonTitle:@"OK" otherButtonTitles:nil];                      [av show];                     }           ];      }  }

Here's what happening above:

  1. You set up the baseURL and the dictionary of parameters to pass to AFHTTPClient.
  2. By registering AFJSONRequestOperation as the HTTP operation, you get free JSON parsing, as in the previous examples.
  3. Here you set up the GET request, with the 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 replace JSON with either of the other two formats discussed previously.

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



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

Hooking into the Live Service

So far you've been calling AFRequestOperations and AFHTTPClient directly from the table view controller. More often than not, your networking requests will be associated with a single web service or API.

AFHTTPClient 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 AFHTTPClient 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 HTTP client subclasses, create a class method that returns a shared singleton instance. This saves resources and eliminates the need to allocate and spin up new objects.

Currently your project doesn't have a subclass of AFHTTPClient, it just creates one directly. Let's fix that to clean up your code.

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

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 "AFHTTPClient.h"     @protocol WeatherHttpClientDelegate;     @interface WeatherHTTPClient : AFHTTPClient     @property(weak) id<WeatherHttpClientDelegate> delegate;     + (WeatherHTTPClient *)sharedWeatherHTTPClient;  - (id)initWithBaseURL:(NSURL *)url;  - (void)updateWeatherAtLocation:(CLLocation *)location forNumberOfDays:(int)number;     @end     @protocol WeatherHttpClientDelegate <NSObject>  -(void)weatherHTTPClient:(WeatherHTTPClient *)client didUpdateWithWeather:(id)weather;  -(void)weatherHTTPClient:(WeatherHTTPClient *)client didFailWithError:(NSError *)error;  @end

You'll learn more about each of these elements as you implement them. Switch over toWeatherHTTPClient.m and add the following just under the @implementation line:

  + (WeatherHTTPClient *)sharedWeatherHTTPClient  {      NSString *urlStr = @"http://free.worldweatheronline.com/feed/";         static dispatch_once_t pred;      static WeatherHTTPClient *_sharedWeatherHTTPClient = nil;         dispatch_once(&pred, ^{ _sharedWeatherHTTPClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:urlStr]]; });      return _sharedWeatherHTTPClient;  }     - (id)initWithBaseURL:(NSURL *)url  {      self = [super initWithBaseURL:url];      if (!self) {          return nil;      }         [self registerHTTPOperationClass:[AFJSONRequestOperation class]];      [self setDefaultHeader:@"Accept" value:@"application/json"];         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 expect JSON responses from the web service.

Paste the following method underneath the previous ones:

  - (void)updateWeatherAtLocation:(CLLocation *)location forNumberOfDays:(int)number{      NSMutableDictionary *parameters = [NSMutableDictionary dictionary];      [parameters setObject:[NSString stringWithFormat:@"%d",number] forKey:@"num_of_days"];      [parameters setObject:[NSString stringWithFormat:@"%f,%f",location.coordinate.latitude,location.coordinate.longitude] forKey:@"q"];      [parameters setObject:@"json" forKey:@"format"];      [parameters setObject:@"7f3a3480fc162445131401" forKey:@"key"];         [self getPath:@"weather.ashx"         parameters:parameters            success:^(AFHTTPRequestOperation *operation, id responseObject) {              if([self.delegate respondsToSelector:@selector(weatherHTTPClient:didUpdateWithWeather:)])                  [self.delegate weatherHTTPClient:self didUpdateWithWeather:responseObject];          }          failure:^(AFHTTPRequestOperation *operation, 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.

VERY IMPORTANT! The API key in this example was created only for this tutorial. If you make an app, please create an account at World Weather Online and get your own API key!

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 can update the WTTableViewController class to take advantage.

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

  #import "WeatherHTTPClient.h"     @interface WTTableViewController : UITableViewController <NSXMLParserDelegate,UIActionSheetDelegate,CLLocationManagerDelegate, WeatherHttpClientDelegate>

Also add a new Core Location manager property:

  @property(strong) CLLocationManager *manager;

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

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

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

  - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation{         //if the location is more than 5 minutes old ignore      if([newLocation.timestamp timeIntervalSinceNow]< 300){          [self.manager 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)aWeather{      self.weather = aWeather;      self.title = @"API Updated";      [self.tableView reloadData];  }     -(void)weatherHTTPClient:(WeatherHTTPClient *)client didFailWithError:(NSError *)error{      UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"                                                   message:[NSString stringWithFormat:@"%@",error]                                                  delegate:nil                                         cancelButtonTitle:@"OK" otherButtonTitles:nil];      [av 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.manager startUpdatingLocation];  }

Build and run your project, 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!

I'm Not Dead Yet!

You might have noticed that this external web service can take some time before it returns with data. It's important to provide your users with feedback when doing network operations so they know the app hasn't stalled or crashed.

Luckily, AFNetworking comes with an easy way to provide this feedback:AFNetworkActivityIndicatorManager.

In WTAppDelegate.m, find the application:didFinishLaunchingWithOptions: method and replace it with the following:

  - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions  {      [AFNetworkActivityIndicatorManager sharedManager].enabled = YES;      return YES;  }

Enabling the sharedManager automatically displays the network activity indicator whenever a new operation is underway. You won't need to manage it separately for every request you make.

Build and run, and you should see the little networking spinner in the status bar whenever there's a network request:



Now there's a sign of life for your user even when your app is waiting on a slow web service!

Downloading Images

If you tap on a table view cell, the app takes you to a detail view of the weather and an animation illustrating the corresponding weather conditions.

That's nice, but at the moment the animation has a very plain background. What better way to update the background than… over the network!

Here's the final AFNetworking trick for this tutorial: AFImageRequestOperation. Like AFJSONRequestOperation, AFImageRequestOperation wraps an HTTP request that expects some kind of image.

There are two method stubs in WeatherAnimationViewController.m to implement. Find theupdateBackgroundImage: method and replace it with the following:

  - (IBAction)updateBackgroundImage:(id)sender {         //Store this image on the same server as the weather canned files      NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.scott-sherwood.com/wp-content/uploads/2013/01/scene.png"]];      AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request          imageProcessingBlock:nil          success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {              self.backgroundImageView.image = image;              [self saveImage:image withFilename:@"background.png"];          }          failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {              NSLog(@"Error %@",error);      }];      [operation start];  }

This method initiates and handles downloading the new background. On completion, it returns the full image requested.

In WeatherAnimationViewController.m, you will see two helper methods, imageWithFilename: and saveImage:withFilename:, which will let you store and load any image you download. updateBackgroundImage: calls these helper methods to save the downloaded images to disk.

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

  - (IBAction)deleteBackgroundImage:(id)sender {      NSString *path;  	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);  	path = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"WeatherHTTPClientImages/"];         NSError *error;      [[NSFileManager defaultManager] removeItemAtPath:path error:&error];         NSString *desc = [self.weatherDictionary weatherDescription];      [self start:desc];  }

This method deletes the downloaded background image so that you can download it again when testing the application.

For the final time: build and run, download the weather data and tap on a cell to get to the detailed view. From here, tap the Update Background button. If you tap on a Sunny cell, you should see this:



Where To Go From Here?

You can download the completed project from here.

Think of all the ways you can now use AFNetworking to communicate with the outside world:

  • AFJSONOperation, AFPropertyListOperation and AFXMLOperation for parsing structured data.
  • UIImageView+AFNetworking for quickly filling in image views.
  • AFHTTPClient for lower-level requests.
  • A custom AFHTTPClient subclass to access a live web service.
  • AFNetworkActivityIndicatorManager to keep the user informed.
  • AFImageRequestOperation for loading images.

The power of AFNetworking is yours to deploy!

If you have any questions about anything you've seen here, please pay a visit to the forums to get some assistance. I'd also love to read your comments!

This is a post by Tutorial Team Member Scott Sherwood, co-founder of Dynamically Loaded, a location-based software company specializing in hybrid-positioning solutions.


iPhoneCategory:

Tags: 

I'd love to hear your thoughts!

No comments:

Post a Comment