在iOS 7中,Apple更新了iOS中的网络基础架构,新推出的网络基础架构是NSURLSession(原来的网络基础架构NSURLConnection)。
iOS开发中往往会涉及网络数据处理,像其他开发环境一样,iOS也提供了网络开发的基础架构(叫做“库”也可以),是谓“Apple原生网络基础架构”,在iOS 7之前,iOS提供的网络架构叫NSURLConnection
;在iOS 7中,Apple更新的iOS中的网络基础架构,新推出的东东叫NSURLSession
;
说明:NSURLSession和NSURLConnection都不是指一个类,而指的是一组构成Foundation框架中URL加载系统的相
互关联的组件,譬如对于NSURLConnection,包
括:NSURLRequest,NSURLResponse,NSURLProtocol,NSURLCache,NSHTTPCookieStorage,NSURLCredentialStorage,
以及和它同名的NSURLConnection。
关于NSURLSession和NSURLConnection更多内容,参考《忘记NSURLConnection,拥抱NSURLSession吧 》。
使用Apple原生网络基础架构
显然能够写出网络处理相关的代码,NSURLSession教程 中有介绍。然而,对于大多数人来说,Apple的原生网络基础架构太难用了,所以实际开发中,我们往往使用第三方开发的网络库(往往是对iOS原生库进行一些包装),AFNetworking就是其中的佼佼者。
最新版本的AFNetworking(即AFNetworking 2.0)正是基于基于iOS 7中推出的NSURLSession,所以,新推出的iOS网络架构NSURLSession
的
种种好处在AFNetworking
2.0中也可得到体现。除了NSURLSession所带来的好处之外,AFNetworking还有一些其他非常酷的特性,譬如序列化
(serialization)、可达性支持(reachability
support)、以及对一些UIKit的功能进行扩展(比如对UIImageView进行了category扩展,使得能够很容易就能够从网络中下载图
片到UIImageView中),
AFNetworking非常受开发者欢迎,它甚至还赢得了2012最佳iOS库大奖
,它也是使用最为广泛的iOS开源项目 。
在本文中,你会通过构建一个天气App
进而了解AFNetworking的主要组件,该App从World Weather Online 中获取数据。刚开始,该App处理的数据是静态(此时不必纠结这里的静态是什么意思,后面你自然明白了),到了最后,数据完全是在线实时数据。
预测:一个很酷的iOS learner即将会掌握所有关于使用AFNetworking的知识并且能够把它应用于自己的App中,这个很酷的iOS learner会是你吗?话不多说,开始干活儿吧!
开始
首先下载我们接下来学习中将要用到的project 。
这个project只提供了基本的UI,目前还不含任何AFNetworking
代码。
打开project的MainStoryboard.storyboard,你会看到如下三个view controllers:
从左到右分别对应:
一个顶层navigation controller; 一个tableview controller,用来显示天气,一行对应一天; 一个自定义的view controller(WeatherAnimationViewController),用来显示一天中的天气详情;
编译并运行project,目前在这个App的UI中你看不到任何有用的天气信息。因为这个App需要的数据来自网络,但是目前还没有添加这部分代码,而完成这些代码正是本文的工作。
首先,到GitHub 中,点击Download Zip
链接下载最新的AFNetworking代码包。
解压下载的文件包,你会看到几个子目录和一些project,我们感兴趣的的是其中的两个目录,AFNetworking
和UIKit+AFNetworking
,如下:
以拖拽方式将上述两个目录添加到project中,如下图:
在添加过程中,会弹出一个对话框,其中有一些添加选项,确保两个选项Copy items into destination group’s folder (if needed)
和Create groups for any added folders
这两个选项都被勾选。 说明:说白了,就是确保那两个目录是以全拷贝
形式添加到工程的,而非以链接形式添加的。
接着,打开project中Supporting Files
目录中的预编译头文件Weather-Prefix.pch
,添加代码到import
声明中,如下:
#import "AFNetworking.h"
把AFNetworking添加到预编译头文件中的目的是,以后使用AFNetworking资源时无需再引入各种头文件。
到目前为止,一切都很容易,难道不是吗?
处理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).
AFNetworking非常强大,普通HTTP请求能够加载和处理结构化数据,AFNetworking也能,除此之外,它还支持JSON、XML和Property Lists(plists)。
你可以下载一些JSON文件,然后自行 通过一些JSON解析工具(譬如iOS内置的NSJSONSerialization)来处理这些JSON文件,但是这样岂不麻烦?AFNetworking能解决这些所有事情。
首先,在接下来的测试中,你需要一个base URL
,添加如下代码到WTTableViewController.m中(放置在import声明式下面即可):
static NSString * const BaseURLString =@"http://www.raywenderlich.com/demos/weather_sample/" ;
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.
这个URL是作者(not笔者)为本文所提供的,类似于提供一个web service
,如果对该URL中的内容比较好奇,可以下载 来看看。
上述的web service
提供三种格式的数据:JSON、XML以及PLIST,它们三个的内容可以从如下链接中看到:
上述的第一种数据格式是JSON,JSON是JavScript衍生出来的对象格式,它一般的形式如下:
{
"data ": {
"current_condition ": [
{
"cloudcover ": "16" ,
"humidity ": "59" ,
"observation_time ": "09:09 PM" ,
}
]
}
}
Note:如果你向对JSON了解更多,可参考JSON教程 。
当用户触碰JSON
按钮,App就会从上述URL中加载和处理JSON数据文件;在WTTableViewController.m文件中,找到jsonTapped:
方法(该方法目前还是空的),填充如下代码:
- (IBAction )jsonTapped:(id )sender
{
NSString * string = [NSString stringWithFormat:@"%@weather.php?format=json" , BaseURLString];
NSURL *url = [NSURL URLWithString:string];
NSURLRequest * request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFJSONResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject){
self .weather = (NSDictionary *)responseObject;
self .title = @"JSON 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];
}
这可能还是你写的第一个AFNetworking代码,很酷对不对?下面对上述代码做下阐述:
首先基于baseURLString创建一个全路径的string URL(指向JSON文件),然后利用该string创建一个NSURL对象url,然后基于url创建一个NSURLRequest对象; AFHTTPRequestOperation是一个all-in-one
类,用户处理network中的HTTP传
输,然后设置AFHTTPRequestOperation对象operation的属性responseSerializer,该属性用于指定JSON
解析器,这里指定的JSON解析器是AFNetworking中的JSON解析器; 指定success block,即若处理成功了,JSON解析器解析接收到的数据并返回一个NSDictionary对象,这里把该对象存在weather中; 指定fail block,即若处理失败了,弹出一个AlertView; 启动operation;
由上面的例子可以看到,AFNetworking使用起来极其简单,仅仅通过几行代码就可以执行一个network任务,并对响应进行处理(即对响应的JSON进行解析)。
OK,到目前为止,从web service
中获取的天气数据被存在self.weather中了,现在你需要把它给展示出来,找到tableView:numberOfRowsInSection:方法,填充如下代码:
- (NSInteger )tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger )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 ;
}
}
这个table view将有两个section,第一个section用于展示当前的天气,第二个section用于展示将来的天气;
你也许会想:“[self.weather upcomingWeather];
是什么玩意儿?”,因为weather是一个NSDictionary
对象,按照我们目前所知道的,它是不能对“upcomingWeather”这个消息响应的。
稍微看一下源码就会发现,project中有两个NSDictionary category
:
NSDictionary+weather NSDictionary+weather_package 这两个categories中包含了一些便利的方法,使得我们可以更容易获取数据元素。对于本文,我们还是应该把重心放在网络这一部分吧,难道不是这样吗?
Note:仅供参考,另一种便利处理JSON数据的方法(而不是想本文这样创建categories)是使用第三方库JSONModel 。
仍然是在WTTableViewController.m中,找到tableView:cellForRowAtIndexPath:
方法并填充代码如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString * CellIdentifier = @"WeatherCell" ;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier
forIndexPath:indexPath];
NSDictionary * daysWeather =nil ;
switch (indexPath.section ) {
case 0 :{
daysWeather = [self .weather currentCondition];
break ;
}
case 1 : {
NSArray *upcomingWeather = [self .weather upcomingWeather];
daysWeather = upcomingWeather[indexPath.row ];
break ;
}
default :
break ;
}
cell.textLabel .text = [daysWeather weatherDescription];
return cell;
}
和tableView:numberOfRowsInSection:
一样,在tableView:cellForRowAtIndexPath:
中也使用到了NSDictionary
的categories提供的方法以便更容易获得数据;当天的天气对应一个dictionary
,将来的天气存在一个array
中。
编译并运行你的project,点击JSON按钮,从web service1
中读取数据,你将看到的UI如下:
显然,这意味着前面的工作一切正常。
处理Property Lists
Property Lists(或者简称plists)其实就是一种XML文件,只是以一种具体的方式组织的,它由Apple推出,Apple在很多地方都用到了它,它长得像这样:
<dict >
<key > data</key >
<dict >
<key > current_condition</key >
<array >
<dict >
<key > cloudcover</key >
<string > 16</string >
<key > humidity</key >
<string > 59</string >
</dict >
</array >
</dict >
</dict >
...
这坨东西代表:
一个dictionary有一个key叫data,其内容是其他的dictionary; 次级的dictionary的key叫current_condition,其内容是一个array; array包含了一个dictionary,该dictionary中有几个key和对应的value;
OK,现在来加载plist文件吧,找到plistTapped:
方法,填充代码如下:
-(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];
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];
}
注意,上述代码除了responseSerializer改变了之外,其他的和前面的JSONTapped:中的代码几乎一样; 这一点的改变使得project现在既能够处理JSON也能够PLIST数据。
编译并运行你的project,点击底部的PLIST按钮,你可以看到如下界面:
顺便说一下,顶部的Clear
按钮可以用于清楚title和table view中的数据,以便重新来过。
处理XML
相对于处理JSON和PLIST格式的数据,处理XML相对来说稍微复杂一点点;这一次,你要自己处理web service的返回结果(前面两个都是用AFNetworking提供的解析器)。 笔者注:以笔者目前短短几个月的经验,处理XML在iOS中并不多见,况且本文的重心是学习AFNetworking,所以余下对XML的处理代码看客可以快速略过,至少不要花太多心思在这个上面了。
幸运的是,iOS内置的NSXMLParser类(它是一种SAM Parser )可以为你解决这个问题。
依然是在WTTableViewController.m文件中,找到xmlTapped:方法,填充如下代码:
- (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];
operation.responseSerializer = [AFXMLParserResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSXMLParser*XMLParser =(NSXMLParser*)responseObject;
[XMLParser setShouldProcessNamespaces:YES ];
} 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];
}
上面的这个代码现在看起来应该非常熟悉了;最大的挑战应该是success block部分代码,在success
block中,得到的不是一个简单的NSDictionary对象,而是一个NSXMLParser对象,后面会用这个对象来解析XML,在执行解析操作
之前,还需要指定NSXMLParser对象的delegate,并实现delegate中定义的方法,这里需要设置
“XMLParser.delegate = self;”。
首先,编辑WTTableViewController.h文件,让WTTableViewController类遵守NSXMLParserDelegate协议,如下:
@interface WTTableViewController : UITableViewController <NSXMLParserDelegate >
显然,这意味着WTTableViewController
这个类要遵守NSXMLParserDelegate
这个协议,接下来要实现该协议中定义的方法,不过在此之前,先添加几个属性吧:
@property (nonatomic , strong ) NSMutableDictionary *currentDictionary;
@property (nonatomic , strong ) NSMutableDictionary *xmlWeather;
@property (nonatomic , strong ) NSString *elementName;
@property (nonatomic , strong ) NSMutableString *outstring;
这几个属性会在解析XML中用到。
然后添加方法到WTTableViewController.m中:
- (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" ]){
NSMutableArray *array = self .xmlWeather [@"weather" ] ? : [NSMutableArray array];
[array addObject:self .currentDictionary ];
self .xmlWeather [@"weather" ] = array;
self .currentDictionary = nil ;
}
else if ([qName isEqualToString:@"value" ]){
}
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 .title = @"XML Retrieved" ;
[self .tableView reloadData];
}
笔者注:原文中对处理XML
的解释非常多,只是在笔者看来非常boring,就此略过了,若看客多这部分代码比较感兴趣,可以看原文 。
OK,三种格式的数据都支持了!哦,对了,别忘了添加XML
按钮的点击事件处理代码,如下:
- (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];
operation.responseSerializer = [AFXMLParserResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject){
NSXMLParser*XMLParser =(NSXMLParser*)responseObject;
[XMLParser setShouldProcessNamespaces:YES ];
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];
}
编译并运行你的project,点击XML
按钮,你会看到如下UI:
让UI更风骚一点
UI目前看起来还比较沉闷,如何让它看起来更棒一点呢?添加图片吧,为每个weather添加一张图片。
再来看一下weather的JSON文件 ,你可以看到文本中每个weather item中分明有image相关的URL,有图片却留着不用,岂不傻瓜?
AFNetworking
为UIImageView
提供的category(即UIImageView+AFNetworking
)提供了异步加载图片的功能,异步下载意味着加载图片的任务放在后台,APP的其他任务仍然保持畅通运行,不会因为加载图片而卡顿,为了要使用UIImageView+AFNetworking
提供的方法,你得添加UIImageView+AFNetworking
头文件,在WTTableViewController.m中添加如下代码:
#import "UIImageView+AFNetworking.h"
找到tableView:cellForRowAtIndexPath:
方法,然后添加代码,即在retrun cell;
前添加如下代码:
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
扩展了setImageWithURLRequest:
等几个相关方法。
setImageWithURLRequest:
中的success block和failure
block都是可选的,但是若指定了success block,你就应该像上述代码一样在success block中指定image
view的image属性,即“weakCell.imageView.image = image;”;或者不指定success block,让setImageWithURLRequest:
方法帮你自动处理; 笔者注:这很容易理解,因为下载图片是异步的,下载的图片会以UIImage的形式存在,下载完成之后当然要指定给对应的UImageView才能达到我们的效果。
当cell(每个weather对应一个cell)第一次被创建时,其image view会显示占位符(placeholder)图片,直到从网络中加载的图片加载完成。
重新build你的project,然后运行,你看到的UI会如下:
OK,目前为止,异步加载图片似乎完成得挺棒。
A RESTful Class
到目前为止,你学会了使用AFHTTPRequestOperation
来处理一次性的HTTP请求;
事实上,除了AFHTTPRequestOperation之外,你还可以使用AFHTTPRequestOperationManager
和AFHTTPSessionManager
,它们能够用来很好的和单个endpoint
(所谓endpoint,譬如http://www.example.com:5000/
就是一个endpoint)的web service
进行通信。
使用AFHTTPSessionManager,你可以设置一个base URL,然后对一个endpoint进行多次http
request,它们都能监测连接变化、对参数进行编码、处理多重表单请求、对批处理进行队列处理,并能够帮助你处理全套的RESTful操作(GET、
POST、PUT以及DELETE)。 笔者注:总之,AFHTTPRequestOperationManager
和AFHTTPSessionManager
你也得掌握,只是前者是iOS 7之前会经常用到,而后者是iOS 7中新推出的,用来代替前者的,至于二者如何选择,就看你的App适配版本了。
你如果像笔者一样对REST不甚了解,可以看看《What is REST 》。
到WTTableViewController.h中作如下更新:
@interface WTTableViewController : UITableViewController <NSXMLParserDelegate ,
CLLocationManagerDelegate, UIActionSheetDelegate]]>
然后在WTTableViewController.m中找到clientTapped:方法,修改它如下:
- (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 ];
}
这个方法比较简单,不多说了,继续添加上面代码中actionSheet的点击事件处理代码:
-(void )actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger )buttonIndex
{
if (buttonIndex == [actionSheet cancelButtonIndex]) {
return ;
}
NSURL *baseURL = [NSURL URLWithString:BaseURLString];
NSDictionary *parameters = @{@"format" :@"json" };
AFHTTPSessionManager *manager =[[AFHTTPSessionManager alloc] initWithBaseURL:baseURL];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
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];
}];
}
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: OK,这个方法略长,对它做些解释:
创建一个base URL,以及在http request中会用到的参数; 创建一个AFHTTPSessionManager实例对象,并设置它的responseSerializer属性为AFNetworking默认的JSON解析器,这和前文JSONTapped:
方法中的设置类似; 如果用户选择的是HTTP GET请求,那么就执行AFHTTPSessionManager的HTTP GET请求; 如果用户选择的是HTTP POST请求,那么就执行AFHTTPSessionManager的HTTP POST请求;
这个示例中请求的是JSON数据,若想请求另外两种数据(PLIST或者XML),也是类似的处理。
重新build你的project并运行app,你看到的UI如下:
到目前为止,你已经掌握了AFHTTPSessionManager,但是,依然可以以更好的方法写出更干净漂亮的代码,你接下来就能学习到…
World Weather Online
恰如其名,全球范围内的天气数据在World Weather Online 中都能找到,通过其提供的API(即HTTP Request)就能够获取,在使用其API之前,你得称为它的用户,先注册 成为它的用户吧,很快就可以完成。
完成注册后,你在注册时所填入的email邮箱会收到一个email,算是认证链接,点击这个链接,然后会跳转到一个页面,然后进入自己的个人页面,获取一个free API key
,记下这个key,后文会用到。
连接weather在线服务
到目前为止,你已经使用AFHTTPRequestOperation
和AFHTTPSessionManager
成
功执行HTTP
REQUEST获得天气数据,然而,上面所做的事实上无非是下载一个文件(譬如JSON文件),然而对这个文件进行解析,换句话说,天气数据是写死的,即
本文刚开始的时候所提到的“静态数据”;在实际应用中,我们不会像本文之前那样下载一个JSON文件,而是通过服务器的API获取事实数据,所谓API,
其实也是一个HTTP请求(只是这个请求中往往需要包含认证信息)。
与服务器(或抽象的称“web
service”)交互的HTTP请求的所有需要的东东AFHTTPSessionManager都有提供,它使得通信处理代码(可谓“连接部分”)和对
数据处理代码(可谓“业务部分”)分开,起到松耦合的作用。看客稍微注意一下就会发现,前文代码的耦合度非常高,把连接处理和数据处理代码都放在一起了。
关于AFHTTPSessionManager的使用,作者列出了两点最佳实践指南:
为每一个web service创建一个子类。譬如,你想实现一个网络资源聚合器,你可能需要为Twitter、Facebook、Instragram(以及其他)各实现一个AFHTTPSessionManager子类; 为每一个AFHTTPSession子类创建一个单例实例(即单例模式),单例模式能够节省资源;
你的project目前还没有AFHTTPSessionManager的子类,来修复这个问题吧。
首先,新建一个类,命名为WeatherHTTPClient,让该类继承AFHTTPSessionManager;
你需要让这个子类干三件事情:处理HTTP requests、得到request请求数据(天气数据)后回调delegate、根据用户的物理位置获取正确的天气数据。
修改WeatherHTTPClient.h文件如下:
#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
在实现这些方法的过程中你会对它们有更深刻的理解。 切换到WeatherHTTPClient.m文件,在import声明后面添加如下代码:
NSString *const WorldWeatherOnlineAPIKey = @"PASTE YOUR API KEY HERE" ;
static NSString * const WorldWeatherOnlineURLString = @"http://api.worldweatheronline.com/free/v1/" ;
注意,刚刚不是让你保存free API key
了吗?现在正是用到它们的时候了,用它来代替上面代码中的PASTE YOUR API KEY HERE
。
接着添加如下代码到@implementation
下面:
+ (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 ;
}
上述代码比较容易理解,稍微说明一下,在sharedWeatherHTTPClient
方法中使用GCD进行了一下保护,至于为什么要这样,具体说明的可以参考让你的单例线程安全 ;然后在初始化方法中指定处理JSON数据JSON解析器。
接着粘贴如下代码:
-(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];
}
}
];
}
这个方法根据指定的位置(location)获取指定时间长度(number)来获取天气数据; 笔者注:updateWeatherAtLocation:
其实是对API进行一下包装。
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.
服务类(WeatherHTTPClient)创建好了,最后的工作就是来使用它了。
打开WTTableViewController.h,让WTTableViewController遵守WeatherHTTPClientDelegate协议:
#import "WeatherHTTPClient.h"
@interface WTTableViewController : UITableViewController <NSXMLParserDelegate ,
CLLocationManagerDelegate, UIActionSheetDelegate, WeatherHTTPClientDelegate]]>
前面不是说好了根据位置获取天气数据吗?位置信息如何获取呢?这个简单,iOS已经集成了这一部分的功能。
添加一个属性:
@property (nonatomic , strong ) CLLocationManager *locationManager;
在WTTableViewController.m文件中,在viewDidLoad:
方法的最后添加两行代码:
self .locationManager =[[CLLocationManager alloc] init ];
self .locationManager.delegate = self ;
这两行代码对locationManager
进行了初始化,并指定了其delegate,接下来就实现该delegate中定义的方法了。
依然是WTTableViewController.m文件中,添加方法:
- (void )locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray*)locations
{
CLLocation *newLocation = [locations lastObject];
if ([newLocation.timestamp timeIntervalSinceNow] > 300 ) {
return ;
}
[self.locationManager stopUpdatingLocation];
WeatherHTTPClient *client = [WeatherHTTPClient sharedWeatherHTTPClient];
client .delegate = self;
[client updateWeatherAtLocation:newLocation forNumberOfDays:5 ];
}
顾名思义,这个方法在定位
完成后被调用,在这个方法中创建了前文创建的类WeatherHTTPClient
的实例,也指定了其delegate,然后调用updateWeatherAtLocation:forNumberOfDays:
方法获取数据;
OK,既然指定了client.delegate = self;
,那么就得继续添加方法了:
- (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];
}
上面定义的两个方法分别在WeatherHTTPClient succeed和fail的时候被调用。
似乎都做完了,真的完了吗?
什么时候启动定位
呢?当然在tap事件中处理了:
- (IBAction )apiTapped:(id )sender
{
[self .locationManager startUpdatingLocation];
}
编译并运行你的project(若你的模拟器不支持定位,就使用真机调试吧,笔者的模拟器是支持定位滴),点击顶部的API按钮你的UI会如下这样:
改善用户体验
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.
你可能也像笔者一样,在天气信息成功都会费点时间,在加载成功之前,屏幕上啥都没有,实际应用中,这会让用户很抓狂的,因为它也不晓得发生了什么事情,你得让他知道点什么,譬如弄个indicator转一转啊。
Luckily, AFNetworking comes with an easy way to provide this feedback: AFNetworkActivityIndicatorManager.
网上提供了很多各种各样的方案,AFNetworking为此提供了简单却有效的解决方案,说“简单”,是因为只需要添加一行代码。
在WTAppDelegate.m文件中找到didFinishLaunchingWithOptions:方法,添加一行代码如下:
- (BOOL )application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[AFNetworkActivityIndicatorManager sharedManager].enabled =YES ;
return YES ;
}
当然,在此之前,你需要添加头文件:
#import "AFNetworkActivityIndicatorManager.h"
重新编译你的project并运行它,你会看到如图这样的东东:
更新天气详情页背景图片
目前,天气详情页面的背景很漂亮,但它不是实时的,若能够从网络上下载背景图片该多好,OK,本文将教你最后一招: AFHTTPRequestOperation不仅能够请求并处理JSON、PLIST等格式化数据,也能够请求和处理图片数据,只是需要设置其responseSerializer属性为AFImageResponseSerializer对象。
打开WeatherAnimationViewController.m文件,找到updateBackgroundImage:方法,实现其代码如下:
- (IBAction )updateBackgroundImage:(id )sender
{
NSURL *url =[NSURL URLWithString:@"http://www.raywenderlich.com/wp-content/uploads/2014/01/sunny-background.png" ];
NSURLRequest *request =[NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *operation =[[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.responseSerializer = [AFImageResponseSerializer serializer];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject){
self .backgroundImageView .image = responseObject;
[self saveImage:responseObject withFilename:@"background.png" ];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
NSLog (@"Error: %@" , error);
}];
[operation start];
}
这个方法从后台下载和处理图片数据。
继续,找到deleteBackgroundImage:方法并实现如下:
-(IBAction )deleteBackgroundImage:(id )sender
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES );
NSString *path = [[paths objectAtIndex:0 ] stringByAppendingPathComponent:@"WeatherHTTPClientImages/" ];
NSError *error = nil ;
[[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. 这个方法会把下载下来的图片给删除掉,以便缓存的图片对测试造成影响,确保每次点击Update Background
按钮的背景图片都是从网上下载而来的,而不是来自本地缓存的。
最后一次,编译并运行你的project,加载天气信息后,选中一个天气,进入天气详细界面,点击Update Background
按钮经过一段时间,发现背景图片会有变化,譬如笔者这样:
接下来呢?
你可以下载本文最终的代码 ; 学习完本文,来总结一下你可以用AFNetworking来干些什么?
结合AFHTTPOperation
和AFJSONResponseSerializer
/AFPropertyListResponseSerializer
/AFXMLParserResponseSerializer
通过HTTP REQUEST请求以及处理JSON/PLIST/XML这些结构化数据; 使用UIImageView+AFNetworking
来异步下载数据; 自定义AFHTTPSessionManager的子类来获取实时的web数据; 使用AFNetworkActivityIndicatorManager来改善用户体验;
总之,AFNetworking的威力由你决定!
最后,看客还是读一下原文 吧!