AFNetworking速成教程

原文地址:http://www.raywenderlich.com/zh-hans/36079/afnetworking速成教程(1)

網絡 — 你的程序離開了它就不能生存下去!蘋果的Foundation framework中的NSURLConnection又非常難以理解, 不過這裏有一個可以使用的替代品:AFNetworking.

AFNetworking 非常受開發者歡迎 – 它贏得了我們讀者的青睞:2012年最佳的iOS Library獎(2012 Best iOS Library Award.) 所以現在我就寫這篇文章來向你介紹如何在程序中有效的使用它。

AFNetworking 包括了所有你需要與在線資源交互的內容,從web services到文件下載。當你的程序在下載一個大文件期間,AFNetworking還能確保你的UI是可以響應的。

本文將介紹AFNetworking框架主要的組成部分。一路上,你將使用World Weather Online提供的諮詢(Feeds)來創建一個天氣(Weather)程序。剛開始使用的天氣數據是靜態的,不過在學完本文內容之後,程序將連接到實時的天氣諮詢。

今日預計:一個很酷的開發者學習所有關於AFNetworking知識,並在他的程序中使用AFNetworking。我們開始忙活吧!

開始

首先來這裏(here)下載本文的啓動項目。這個工程提供了一個基本的UI — AFNetworking相關代碼還沒有添加。

打開MainStoryboard.storyboard文件,將看到3個view controller:


從左到右,分別是:

  • 頂級(top-level)的導航控制器;
  • 用來顯示天氣的一個table view controller,每天一行;
  • 一個自定義的view controller (WeatherAnimationViewController) 當用戶點擊某個table view cell時,這個view controller將顯示某一天的天氣諮詢。

生成並運行項目,你將看到相關的UI出現,但是什麼都沒有實現!因爲程序需要從網絡中獲取到所需要的數據,而相關代碼還沒有添加。這也是本文中你將要實現的!

首先,你需要將AFNetworking 框架包含到工程中。如果你還沒有AFNetworking的話,在這裏下載最新的版本:GitHub.

當你解壓出下載的文件後,你將看到其中有一個AFNetworking子文件夾,裏面全是.h 和 .m 文件, 如下高亮顯示的:


AFNetworking拖拽到Xcode工程中.


當出現了添加文件的選項時,確保勾選上Copy items into destination group’s folder (if needed) 和 Create groups for any added folders.

要完成相關配置,請在工程的Supporting Files羣組中打開預編譯頭文件Weather-Prefix.pch. 然後在別的import後面添加如下一行代碼:

#import "AFNetworking.h"

將AFNetworking添加到預編譯頭文件,意味着這個框架會被自動的添加到工程的所有源代碼文件中。

很容易,不是嗎?現在你已經準備好“天氣”程序代碼了!

操作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可以幫助你進行網絡開發!


發佈了8 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章