• AFNetworking速成教程


    zhuan :http://www.raywenderlich.com/zh-hans/36079/afnetworking%E9%80%9F%E6%88%90%E6%95%99%E7%A8%8B%EF%BC%881%EF%BC%89

    操作JSON

    AFNetworking通过网络来加载和处理结构化的数据是非常智能的,普通的HTTP请求也一样。尤其是它支持JSON, XML 和 Property Lists (plists).

    你可以下载一些JSON数据,然后用自己的解析器来解析,但这何必呢?通过AFNetworking就可以完成这些操作!

    首先,你需要测试脚本(数据)所需的一个基本URL。将下面的这个静态NSString声明到WTTableViewController.m顶部,也就是所有#import下面:

    static NSString *const BaseURLString = @"http://www.raywenderlich.com/downloads/weather_sample/";

    这个URL是一个非常简单的“web service”,在本文中我特意为你创建的。如果你想知道它看起来是什么样,可以来这里下载代码:download the source.

    这个web service以3种不同的格式(JSON, XML 和 PLIST)返回天气数据。你可以使用下面的这些URL来看看返回的数据:

    第一个数据格式使用的是JSON. JSON 是一种常见的JavaScript派生类对象格式。看起来如下:

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

    注意: 如果你想要结更多关于JSON内容,请参考:Working with JSON in iOS 5 Tutorial.

    当用户点击程序中的JSON按钮时,你希望对从服务中获得的JSON数据进行加载并处理。在WTTableViewController.m中,找到jsonTapped: 方法 (现在应该是空的) ,并用下面的代码替换:

    - (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];
    }

    这是你的第一个AFNetworking代码!因此,这看起来是全新的,我将对这个方法中代码进行介绍。

    1. 根据基本的URL构造出完整的一个URL。然后通过这个完整的URL获得一个NSURL对象,然后根据这个url获得一个NSURLRequest.
    2. AFJSONRequestOperation 是一个功能完整的类(all-in-one)— 整合了从网络中获取数据并对JSON进行解析。
    3. 当请求成功,则运行成功块(success block)。在本示例中,把解析出来的天气数据从JSON变量转换为一个字典(dictionary),并将其存储在属性 weather 中.
    4. 如果运行出问题了,则运行失败块(failure block),比如网络不可用。如果failure block被调用了,将会通过提示框显示出错误信息。

    如上所示,AFNetworking的使用非常简单。如果要用苹果提供的APIs(如NSURLConnection)来实现同样的功能(下载和解析JSON数据),则需要许多代码才能做到。

    现在天气数据已经存在于self.weather中,你需要将其显示出来。找到tableView:numberOfRowsInSection: 方法,并用下面的代码替换:

    - (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;
        }
    }

    table view有两个section:第一个用来显示当前天气,第二个用来显示未来的天气。

    等一分钟,你可能正在思考。这里的 [self.weather upcomingWeather]是什么? 如果self.weather是一个普通的NSDictionary, 它是怎么知道 “upcomingWeather” 是什么呢?

    为了更容易的解析数据,在starter工程中,有一对NSDictionary categories:

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

    这些categories添加了一些方便的方法,通过这些方法可以很方便的对字典中的数据元素进行访问。这样你就可以专注于网络部分,而不是NSDictionary中数据的访问。对吧?

    回到 WTTableViewController.m, 找到 tableView:cellForRowAtIndexPath: 方法,并用下面的实现替换:

    - (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;
    }

    跟tableView:numberOfRowsInSection: 方法一样,在这里使用了便利的NSDictionary categories来获得数据。当前天的天气是一个字典,而未来几日的天气则存储在一个数组中。

    生成并运行工程,然后点击JSON按钮. 这将会动态的获得一个AFJSONOperation对象, 并看到如下画面内容:


    JSON 操作成功!

    操作Property Lists(plists)

    Property lists (或简称为 plists) 是以确定的格式(苹果定义的)构成的XML文件。苹果一般将plists用在用户设置中。看起来如下:

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

    上面的意思是:

    • 一个字典中有一个名为“data”的key,这个key对应着另外一个字典。
    • 这个字典有一个名为 “current_condition” 的key,这个key对应着一个array.
    • 这个数组包含着一个字典,字典中有多个key和values。比如cloudcover=16和humidity=59.

    现在是时候加载plist版本的天气数据了!找到plistTapped: 方法,并用下面的实现替换:

     -(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];
    }

    注意到,上面的代码几乎与JSON版的一致,只不过将操作(operation)的类型从AFJSONOperation 修改为 AFPropertyListOperation. 这非常的整齐:你才程序只需要修改一丁点代码就可以接收JSON或plist格式的数据了!

    生成并运行工程,然后点击PLIST按钮。将看到如下内容:


    如果你需要重置所有的内容,以重新开始操作,导航栏顶部的Clear按钮可以清除掉title和table view中的数据。

    操作XML

    AFNetworking处理JSON和plist的解析使用的是类似的方法,并不需要花费太多功夫,而处理XML则要稍微复杂一点。下面,就根据XML咨询构造一个天气字典(NSDictionary)。

    iOS提供了一个帮助类:NSXMLParse (如果你想了解更多内容,请看这里的链接:SAX parser).

    还是在文件WTTableViewController.m, 找到 xmlTapped: 方法,并用下面的实现替换:

    - (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];
    }

    到现在为止,这看起来跟之前处理JSON和plist很类似。最大的改动就是在成功块(success block)中, 在这里不会传递给你一个预处理好的NSDictionary对象. 而是AFXMLRequestOperation实例化的NSXMLParse对象,这个对象将用来处理繁重的XML解析任务。

    NSXMLParse对象有一组delegate方法是你需要实现的 — 用来获得XML数据。注意,在上面的代码中我将XMLParser的delegate设置为self, 因此WTTableViewController将用来处理XML的解析任务。

    首先,更新一下WTTableViewController.h 并修改一下类声明,如下所示:

    @interface WTTableViewController : UITableViewController&lt;NSXMLParserDelegate&gt;

    上面代码的意思是这个类将实现(遵循)NSXMLParserDelegate协议. 下一步将下面的delegate方法声明添加到@implementation后面:

    - (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;

    为了支持资讯的解析,还需要一些属性来存储相关的数据。将下面的代码添加到@implementatio后面:

    @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;

    接着打开WTTableViewController.m,现在你需要一个一个的实现上面所说的几个delegate方法。将下面这个方法粘贴到实现文件中:

    - (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];
    }

    当NSXMLParser发现了新的元素开始标签时,会调用上面这个方法。在这个方法中,在构造一个新字典用来存储赋值给currentDictionary属性之前,首先保存住上一个元素名称。还要将outstring重置一下,这个字符串用来构造XML标签中的数据。

    然后将下面这个方法粘贴到上一个方法的后面:

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

    如名字一样,当NSXMLParser在一个XML标签中发现了字符数据,会调用这个方法。该方法将字符数据追加到outstring属性中,当XML标签结束的时候,这个outstring会被处理。

    继续,将下面这个方法粘贴到上一个方法的后面:

    - (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;
    }

    当检测到元素的结束标签时,会调用上面这个方法。在这个方法中,会查找一些标签:

    1. urrent_condition 元素表示获得了一个今天的天气。会把今天的天气直接添加到xmlWeather字典中。
    2. weather 元素表示获得了随后一天的天气。今天的天气只有一个,而后续的天气有多个,所以在此,将后续天气添加到一个数组中。
    3. value 标签出现在别的标签中,所以这里可以忽略掉这个标签。
    4. weatherDesc 和 weatherIconUrl 元素的值在存储之前,需要需要被放入一个数组中 — 这里的结构是为了与JSON和plist版本天气咨询格式相匹配。
    5. 所有其它元素都是按照原样(as-is)进行存储的。

    下面是最后一个delegate方法!将下面这个方法粘贴到上一个方法的后面:

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

    当NSXMLParser解析到document的尾部时,会调用这个方法。在此,xmlWeather字典已经构造完毕,table view可以重新加载了。

    在上面代码中将xmlWeather添加到一个字典中,看起来是冗余的, 不过这样可以确保与JSON和plist版本的格式完全匹配。这样所有的3种数据格式(JSON, plist和XML)都能够用相同的代码来显示!

    现在所有的delegate方法和属性都搞定了,找到xmlTapped: 方法,并取消注释成功块(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;
        ...
    }

    生成和运行工程,然后点击XML按钮,将看到如下内容:


    一个小的天气程序

    嗯, 上面的这个程序看起来体验不太友好,有点像整周都是阴雨天。如何让table view中的天气信息体验更好点呢?

    再仔细看看之前的JSON格式数据:JSON format from before,你会看到每个天气项里面都有一个图片URLs。 将这些天气图片显示到每个table view cell中,这样程序看起来会更有意思。

    AFNetworking给UIImageView添加了一个category,让图片能够异步加载,也就是说当图片在后台下载的时候,程序的UI界面仍然能够响应。为了使用这个功能,首先需要将这个category import到WTTableViewController.m文件的顶部:

    #import "UIImageView+AFNetworking.h"
    找到tableView:cellForRowAtIndexPath: 方法,并将下面的代码粘贴到最后的return cell; 代码上上面(这里应该有一个注释标记)
    __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){
     
                                   }];

    首先创建一个弱引用(weak)的cell,这样就可以在block中使用这个cell。如果你直接访问cell变量,Xcode会提示一个关于retain循环和内存泄露的警告。

    UIImageView+AFNetworking category定义了一个setImageWithURLRequest… 方法. 这个方法的参数包括:一个指向图片URL的请求,一个占位符图片,一个success block和一个failure block。

    当cell首次被创建的时候,cell中的UIImageView将显示一个占位符图片,直到真正的图片被下载完成。在这里你需要确保占位符的图片与实际图片尺寸大小相同。

    如果尺寸不相同的话,你可以在success block中调用cell的setNeedsLayout方法. 上面代码中对两行代码进行了注释,这是因为这里的占位符图片尺寸正好合适,留着注释,可能在别的程序中需要用到。

    现在生成并运行工程,然后点击之前添加的3个操作中的任意一个,将看到如下内容:


    很好! 异步加载图片从来没有这么简单过。

    一个RESTful类

    到现在你已经使用类似AFJSONRequestOperation这样的类创建了一次性的HTTP请求。另外,较低级的AFHTTPClient类是用来访问单个的web service终端。 对这个AFHTTPClient一般是给它设置一个基本的URL,然后用AFHTTPClient进行多个请求(而不是像之前的那样,每次请求的时候,都创建一个AFHTTPClient)。

    AFHTTPClient同样为编码参数、处理multipart表单请求body的构造、管理请求操作和批次入队列操作提供了很强的灵活性,它还处理了整套RESTful (GET, POST, PUT, 和 DELETE), 下面我们就来试试最常用的两个:GET 和 POST.

    注意: 对REST, GET和POST不清楚?看看这里比较有趣的介绍 – 我如何给妻子解释REST(How I Explained REST to My Wife.)

    在WTTableViewController.h 顶部将类声明按照如下修改:

    @interface WTTableViewController : UITableViewController

    在 WTTableViewController.m中,找到httpClientTapped: 方法,并用下面的实现替换:

    - (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];
    }

    上面的方法会弹出一个action sheet,用以选择GET和POST请求。粘贴如下代码以实现action sheet中按钮对应的操作:

    - (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];
     
                    }
             ];
        }
    }

    上面的代码作用如下:

    1. 构建一个baseURL,以及一个参数字典,并将这两个变量传给AFHTTPClient.
    2. 将AFJSONRequestOperation注册为HTTP的操作, 这样就可以跟之前的示例一样,可以获得解析好的JSON数据。
    3. 做了一个GET请求,这个请求有一对block:success和failure。
    4. POST请求跟GET一样。

    在这里,将请求一个JSON回应,当然也可以使用之前讨论过的另外两种格式来代替JSON。

    生成并运行工程,点击HTTPClient按钮,然后选择GET 或 POST按钮来初始化一个相关的请求。之后会看到如下内容:


    至此,你已经知道AFHTTPClient最基本的使用方法。不过,这里还有更好的一种使用方法,它可以让代码更加干净整齐,下面我们就来学习一下吧。

    连接到Live Service

    到现在为止,你已经在table view controller中直接调用了AFRequestOperations 和 AFHTTPClient. 实际上,大多数时候不是这样的,你的网络请求会跟某个web service或API相关。

    AFHTTPClient已经具备与web API通讯的所有内容。AFHTTPClient在代码中已经把网络通讯部分做了解耦处理,让网络通讯的代码在整个工程中都可以重用。

    下面是两个关于AFHTTPClient最佳实践的指导:

    1. 为每个web service创建一个子类。例如,如果你在写一个社交网络聚合器,那么可能就会有Twitter的一个子类,Facebook的一个子类,Instragram的一个子类等等。
    2. 在AFHTTPClient子类中,创建一个类方法,用来返回一个共享的单例,这将会节约资源并省去必要的对象创建。

    当前,你的工程中还没有一个AFHTTPClient的子类,下面就来创建一个吧。我们来处理一下,让代码清洁起来。

    首先,在工程中创建一个新的文件:iOSCocoa TouchObjective-C Class. 命名为WeatherHTTPClient 并让其继承自AFHTTPClient.

    你希望这个类做3件事情:

    A:执行HTTP请求

    B:当有新的可用天气数据时,调用delegate

    C:使用用户当前地理位置来获得准确的天气。

    用下面的代码替换WeatherHTTPClient.h:

    #import "AFHTTPClient.h"
     
    @protocol WeatherHttpClientDelegate;
     
    @interface WeatherHTTPClient : AFHTTPClient
     
    @property(weak) id delegate;
     
    + (WeatherHTTPClient *)sharedWeatherHTTPClient;
    - (id)initWithBaseURL:(NSURL *)url;
    - (void)updateWeatherAtLocation:(CLLocation *)location forNumberOfDays:(int)number;
     
    @end
     
    @protocol WeatherHttpClientDelegate 
    -(void)weatherHTTPClient:(WeatherHTTPClient *)client didUpdateWithWeather:(id)weather;
    -(void)weatherHTTPClient:(WeatherHTTPClient *)client didFailWithError:(NSError *)error;
    @end

    在实现文件中,你将了解头文件中定义的更多相关内容。打开WeatherHTTPClient.m 并将下面的代码添加到@implementation下面:

    + (WeatherHTTPClient *)sharedWeatherHTTPClient
    {
        NSString *urlStr = @"http://free.worldweatheronline.com/feed/";
     
        static dispatch_once_t pred;
        static WeatherHTTPClient *_sharedWeatherHTTPClient = nil;
     
        dispatch_once(&amp;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;
    }

    sharedWeatherHTTPClient 方法使用Grand Central Dispatch(GCD)来确保这个共享的单例对象只被初始化分配一次。这里用一个base URL来初始化对象,并将其设置为期望web service响应为JSON。

    将下面的方法粘贴到上一个方法的下面:

    - (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];
            }];
    }

    这个方法调用World Weather Online接口,以获得具体位置的天气信息。

    非常重要!本实例中的API key仅仅是为本文创建的。如果你创建了一个程序,请在World Weather Online创建一个账号,并获得你自己的API key!

    一旦对象获得了天气数据,它需要一些方法来通知对此感兴趣的对象:数据回来了。这里要感谢WeatherHttpClientDelegate 协议和它的delegate方法,在上面代码中的success 和 failure blocks可以通知一个controller:指定位置的天气已经更新了。这样,controller就可以对天气做更新显示。

    现在,我们需要把这些代码片段整合到一起!WeatherHTTPClient希望接收一个位置信息,并且WeatherHTTPClient定义了一个delegate协议,现在对WTTableViewControlle类做一下更新,以使用WeatherHTTPClient.

    打开WTTableViewController.h 添加一个import,并用下面的代码替换@interface声明:

    #import "WeatherHTTPClient.h"
     
    @interface WTTableViewController : UITableViewController

    另外添加一个新的Core Location manager 属性:

    @property(strong) CLLocationManager *manager;

    在 WTTableViewController.m中,将下面的代码添加到viewDidLoad:的底部:

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

    上面这两行代码初始化了Core Location manager,这样当view加载的时候,用来确定用户的当前位置。Core Location然后会通过delegate回调以传回位置信息。将下面的方法添加到实现文件中:

    - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation{
     
        //if the location is more than 5 minutes old ignore
        if([newLocation.timestamp timeIntervalSinceNow]&lt; 300){
            [self.manager stopUpdatingLocation];
     
            WeatherHTTPClient *client = [WeatherHTTPClient sharedWeatherHTTPClient];
            client.delegate = self;
            [client updateWeatherAtLocation:newLocation forNumberOfDays:5];  
        }
    }

    现在,当用户的位置有了变化时,你就可以使用WeatherHTTPClient单例来请求当前位置的天气信息。

    记住,WeatherHTTPClient有两个delegate方法需要实现。将下面两个方法添加到实现文件中:

    -(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];
    }

    上面的两个方法,当WeatherHTTPClient请求成功, 你就可以更新天气数据并重新加载table view。如果网络错误,则显示一个错误信息。

    找到apiTapped: 方法,并用下面的方法替换:

    -(IBAction)apiTapped:(id)sender{
        [self.manager startUpdatingLocation];
    }

    生成并运行程序,点击AP按钮以初始化一个WeatherHTTPClient 请求, 然后会看到如下画面:


    希望在这里你未来的天气跟我的一样:晴天!

    我还没有死!

    你可能注意到了,这里调用的外部web service需要花费一些时间才能返回数据。当在进行网络操作时,给用户提供一个信息反馈是非常重要的,这样用户才知道程序是在运行中或已奔溃了。

    很幸运的是,AFNetworking有一个简便的方法来提供信息反馈:AFNetworkActivityIndicatorManager.

    在 WTAppDelegate.m中,找到application:didFinishLaunchingWithOptions: 方法,并用下面的方法替换:

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

    让sharedManager可以自动的显示出网络活动指示器( network activity indicator)— 无论射门时候,只要有一个新的网络请求在后台运行着。 这样你就不需要每次请求的时候,都要单独进行管理。

    生成并运行工程,无论什么时候,只要有网络请求,都可以在状态栏中看到一个小的网络风火轮:


    现在,即使你的程序在等待一个很慢的web service,用户都知道程序还在运行着!

    下载图片

    如果你在table view cell上点击,程序会切换到天气的详细画面,并且以动画的方式显示出相应的天气情况。

    这非常不错,但目前动画只有一个背景图片。除了通过网络来更新背景图片,还有更好的方法吗!

    下面是本文关于介绍AFNetworking的最后内容了:AFImageRequestOperation. 跟AFJSONRequestOperation一样, AFImageRequestOperation封装了HTTP请求:获取图片。

    在WeatherAnimationViewController.m 中有两个方法需要实现. 找到updateBackgroundImage: 方法,并用下面的代码替换:

    - (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];
    }

    这个方法初始化并下载一个新的背景图片。在结束时,它将返回请求到的完整图片。

    在WeatherAnimationViewController.m中, 你将看到两个辅助方法:imageWithFilename: 和 saveImage:withFilename:, 通过这两个辅助方法,可以对下载下来的图片进行存储和加载。updateBackgroundImage: 将通过辅助方法把下载的图片存储到磁盘中。

    接下来找到deleteBackgroundImage: 方法,并用下面的代码替换:

    - (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:&amp;error];
     
        NSString *desc = [self.weatherDictionary weatherDescription];
        [self start:desc];
    }

    这个方法将删除已经下载的图片,这样在测试程序的时候,你可以再次下载图片。

    最后一次:生成并运行工程,下载天气数据,并点击某个cell,以打开详细天气画面。在详细天气画面中,点击Update Background 按钮. 如果你点击的是晴天cell,将会看到如下画面:


    你可以在这里下载到完整的工程:here.

    你所想到的所有方法,都可以使用AFNetworking来与外界通讯:

    • AFJSONOperation, AFPropertyListOperation 和 AFXMLOperation用来解析结构化数据。
    • UIImageView+AFNetworking用来快捷的填充image view。
    • AFHTTPClient用来进行更底层的请求。
    • 用自定义的AFHTTPClient子类来访问一个web service。
    • AFNetworkActivityIndicatorManager用来给用户做出网络访问的提示。
    • AFImageRequestOperation用来加载图片。

    AFNetworking可以帮助你进行网络开发!

    如果你有任何相关问题,请访问我们的网站以获得帮助。我也很乐意看到你的回复!

  • 相关阅读:
    [算法导论]红黑树实现(插入和删除) @ Python
    [算法导论]二叉查找树的实现 @ Python
    [leetcode] Min Stack @ Python
    [leetcode]Find Minimum in Rotated Sorted Array II @ Python
    [leetcode]Find Minimum in Rotated Sorted Array @ Python
    [leetcode]Maximum Product Subarray @ Python
    业余办一个【编程语言+数据结构+算法】培训班怎么样?
    [算法导论]merge sort @ Python
    聚合页是什么?网站聚合页如何做?网站聚合页SEO完全实践指南
    SEO实验:相关性的搜索结果静态页面是否也会在百度劲风算法的处理之中?
  • 原文地址:https://www.cnblogs.com/AbelChen1991/p/3841511.html
Copyright © 2020-2023  润新知