@Xiaojun-Jin
2014-09-10T09:19:27.000000Z
字数 10639
阅读 3285
iOS
AFNetworking
Note from author: This is a learning note of AFNetworking 2.0 Tutorial written by Ray Wenderlich.
AFNetworking is built on top of NSURLSession, so you get all of the great features provided there. But you also get a lot of extra cool features – like serialization, reachability support, UIKit integration (such as a handy category on asynchronously loading images in a UIImageView), and more.
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 can take a look at the sample data format at this URL:
http://www.raywenderlich.com/demos/weather_sample/weather.php?format=json
NSString *str = [NSString stringWithFormat:@"%@weather.php?format=json", BaseURLString];
NSURL *url = [NSURL URLWithString:str];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:req];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id resObj)
{
self.weather = (NSDictionary *)resObj; [self.tableView reloadData];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
// Display [error localizedDescription] with an alert view
}];
[operation start];
NSString *str = [NSString stringWithFormat:@"%@weather.php?format=plist", BaseURLString];
// Make sure to set the responseSerializer correctly
operation.responseSerializer = [AFPropertyListResponseSerializer serializer];
The code for Plists is almost identical to the JSON version, except for changing the responseSerializer to AFPropertyListResponseSerializer
to let AFNetworking know that you're going to be parsing a plist.
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.
NSString *str = [NSString stringWithFormat:@"%@weather.php?format=xml", BaseURLString];
...
operation.responseSerializer = [AFXMLParserResponseSerializer serializer];
...
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id resObj)
{
NSXMLParser *XMLParser = (NSXMLParser *)resObj;
[XMLParser setShouldProcessNamespaces:YES];
XMLParser.delegate = self; [XMLParser parse];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
// Display [error localizedDescription] with an alert view
}];
NSMutableDictionary *currentDictionary; // current section being parsed
NSMutableDictionary *xmlWeather; // completed parsed xml response
#pragma mark - NSXMLParserDelegate
- (void)parserDidStartDocument:(NSXMLParser *)parser
{
self.xmlWeather = [NSMutableDictionary dictionary];
}
- (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];
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
if (!self.elementName) return;
[self.outstring appendFormat:@"%@", string];
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
{
if ([qName isEqualToString:@"current_condition"] ||
[qName isEqualToString:@"request"])
{
self.xmlWeather[qName] = @[self.currentDictionary];
self.currentDictionary = nil;
}
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;
}
else if ([qName isEqualToString:@"value"])
{
// Ignore value tags, they only appear in the two conditions below
}
else if ([qName isEqualToString:@"weatherDesc"] ||
[qName isEqualToString:@"weatherIconUrl"])
{
NSDictionary *dictionary = @{@"value": self.outstring};
NSArray *array = @[dictionary];
self.currentDictionary[qName] = array;
}
else if (qName)
{
self.currentDictionary[qName] = self.outstring;
}
self.elementName = nil;
}
- (void)parserDidEndDocument:(NSXMLParser *)parser
{
self.weather = @{@"data": self.xmlWeather};[self.tableView reloadData];
}
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.
NSURL *url = [NSURL URLWithString:daysWeather.weatherIconURL];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
UIImage *placeholderImage = [UIImage imageNamed:@"placeholder"];
[cell.imageView setImageWithURLRequest:request
placeholderImage:placeholderImage
success:^(NSURLRequest *request,
NSHTTPURLResponse *response,
UIImage *image)
{
weakCell.imageView.image = image;
}
failure:nil];
AFHTTPRequestOperationManager
and AFHTTPSessionManager
are designed to help you easily interact with a single, web-service endpoint. Both of these allow you to make several requests to the same endpoint and perform the full suite of RESTful verbs (GET, POST, PUT, and DELETE).
If you're targeting iOS 7 and above, use
AFHTTPSessionManager
, while you should useAFHTTPRequestOperationManager
if you are going to be compatible with iOS 6.
NSURL *baseURL = [NSURL URLWithString:BaseURLString];
NSDictionary *parameters = @{@"format": @"json"};
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
if (buttonIndex == 0) // HTTP GET
{
[manager GET:@"weather.php" parameters:parameters
success:^(NSURLSessionDataTask *task, id responseObject)
{
self.weather = responseObject; [self.tableView reloadData];
}
failure:^(NSURLSessionDataTask *task, NSError *error)
{
// Display [error localizedDescription] with an alert view
}];
}
else if (buttonIndex == 1) // HTTP POST
{
[manager POST:@"weather.php" parameters:parameters
success:^(NSURLSessionDataTask *task, id responseObject)
{
self.weather = responseObject; [self.tableView reloadData];
}
failure:^(NSURLSessionDataTask *task, NSError *error)
{
// Display [error localizedDescription] with an alert view
}];
}
Here are two guidelines on AFHTTPSessionManager best practices:
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.
In each AFHTTPSessionManager subclass, create a class method that returns a singleton instance.
Now, let's create a new class file and make it as a subclass of AFHTTPSessionManager
. We'll want this 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.
#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
Next replace the contents of WeatherHTTPClient.m with the following:
static NSString * const WeatherOnlineAPIKey = @"PASTE YOUR API KEY HERE";
static NSString * const WeatherOnlineURL = @"http://api.worldweatheronline.com/free/v1/";
@implementation WeatherHTTPClient
+ (WeatherHTTPClient *)sharedWeatherHTTPClient
{
static WeatherHTTPClient *_sharedWeatherHTTPClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
NSURL *URL = [NSURL URLWithString:WeatherOnlineURL];
_sharedWeatherHTTPClient = [[self alloc] initWithBaseURL:URL];
});
return _sharedWeatherHTTPClient;
}
- (instancetype)initWithBaseURL:(NSURL *)url
{
self = [super initWithBaseURL:url];
if (self)
{
self.responseSerializer = [AFJSONResponseSerializer serializer];
self.requestSerializer = [AFJSONRequestSerializer serializer];
}
return self;
}
- (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"] = WeatherOnlineAPIKey;
[self GET:@"weather.ashx" parameters:parameters
success:^(NSURLSessionDataTask *task, id responseObject)
{
[self.delegate weatherHTTPClient:self didUpdateWithWeather:responseObject];
}
failure:^(NSURLSessionDataTask *task, NSError *error)
{
[self.delegate weatherHTTPClient:self didFailWithError:error];
}];
}
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. Also, we have to add a Core Location to determine the user's location.
@property (nonatomic, strong) CLLocationManager *locationManager;
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
- (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];
}
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.
- (IBAction)apiTapped:(id)sender
{
[self.locationManager startUpdatingLocation];
}
Here's the final AFNetworking trick for this tutorial: AFHTTPRequestOperation
can also handle image requests by setting its responseSerializer to an instance of AFImageResponseSerializer.
NSString *URLStr = @"http://www.raywenderlich.com/sunny-background.png";
NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:URLStr]];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:req];
operation.responseSerializer = [AFImageResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id resObj)
{
self.backgroundImageView.image = resObj;
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(@"Error: %@", error);
}];
[operation start];
AFHTTPOperation
withAFJSONResponseSerializer
,AFPropertyListResponseSerializer
, orAFXMLParserResponseSerializer
response serializers for parsing structured dataUIImageView+AFNetworking
for quickly filling in image views- Custom
AFHTTPSessionManager
subclasses to access live web servicesAFNetworkActivityIndicatorManager
to keep the user informedAFHTTPOperation
with aAFImageResponseSerializer
response serializer for loading images