原文地址: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後面添加如下一行代碼:
將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按鈕時,你希望對從服務中獲得的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代碼!因此,這看起來是全新的,我將對這個方法中代碼進行介紹。
-
根據基本的URL構造出完整的一個URL。然後通過這個完整的URL獲得一個NSURL對象,然後根據這個url獲得一個NSURLRequest.
-
AFJSONRequestOperation 是一個功能完整的類(all-in-one)— 整合了從網絡中獲取數據並對JSON進行解析。
-
當請求成功,則運行成功塊(success block)。在本示例中,把解析出來的天氣數據從JSON變量轉換爲一個字典(dictionary),並將其存儲在屬性 weather 中.
-
如果運行出問題了,則運行失敗塊(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<NSXMLParserDelegate>
|
上面代碼的意思是這個類將實現(遵循)NSXMLParserDelegate協議. 下一步將下面的delegate方法聲明添加到@implementation後面:
爲了支持資訊的解析,還需要一些屬性來存儲相關的數據。將下面的代碼添加到@implementatio後面:
接着打開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;
}
|
當檢測到元素的結束標籤時,會調用上面這個方法。在這個方法中,會查找一些標籤:
-
urrent_condition 元素表示獲得了一個今天的天氣。會把今天的天氣直接添加到xmlWeather字典中。
-
weather 元素表示獲得了隨後一天的天氣。今天的天氣只有一個,而後續的天氣有多個,所以在此,將後續天氣添加到一個數組中。
-
value 標籤出現在別的標籤中,所以這裏可以忽略掉這個標籤。
-
weatherDesc 和 weatherIconUrl 元素的值在存儲之前,需要需要被放入一個數組中
— 這裏的結構是爲了與JSON和plist版本天氣諮詢格式相匹配。
-
所有其它元素都是按照原樣(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)中的一行代碼:
生成和運行工程,然後點擊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.
在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];
}
];
}
}
|
上面的代碼作用如下:
-
構建一個baseURL,以及一個參數字典,並將這兩個變量傳給AFHTTPClient.
-
將AFJSONRequestOperation註冊爲HTTP的操作, 這樣就可以跟之前的示例一樣,可以獲得解析好的JSON數據。
-
做了一個GET請求,這個請求有一對block:success和failure。
-
POST請求跟GET一樣。
在這裏,將請求一個JSON迴應,當然也可以使用之前討論過的另外兩種格式來代替JSON。
生成並運行工程,點擊HTTPClient按鈕,然後選擇GET 或 POST按鈕來初始化一個相關的請求。之後會看到如下內容:
至此,你已經知道AFHTTPClient最基本的使用方法。不過,這裏還有更好的一種使用方法,它可以讓代碼更加乾淨整齊,下面我們就來學習一下吧。
連接到Live Service
到現在爲止,你已經在table view controller中直接調用了AFRequestOperations 和 AFHTTPClient. 實際上,大多數時候不是這樣的,你的網絡請求會跟某個web service或API相關。
AFHTTPClient已經具備與web API通訊的所有內容。AFHTTPClient在代碼中已經把網絡通訊部分做了解耦處理,讓網絡通訊的代碼在整個工程中都可以重用。
下面是兩個關於AFHTTPClient最佳實踐的指導:
-
爲每個web service創建一個子類。例如,如果你在寫一個社交網絡聚合器,那麼可能就會有Twitter的一個子類,Facebook的一個子類,Instragram的一個子類等等。
-
在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(&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接口,以獲得具體位置的天氣信息。
一旦對象獲得了天氣數據,它需要一些方法來通知對此感興趣的對象:數據回來了。這裏要感謝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]< 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:&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可以幫助你進行網絡開發!