大部分應用程序都或多或少會牽扯到網絡開發,例如說新浪微博、微信等,這些應用本身可能采用iOS開發,但是所有的數據支撐都是基於後台網絡服務器的。如今,網絡編程越來越普遍,孤立的應用通常是沒有生命力的。今天就會給大家介紹這部分內容:
做過Web開發的朋友應該很清楚,Http是無連接的請求。每個請求request服務器都有一個對應的響應response,無論是asp.net、jsp、php都是基於這種機制開發的。
在Web開發中主要的請求方法有如下幾種:
在開發中往往數據存儲在服務器端,而客戶端(iOS應用)往往通過向服務器端發送請求從服務器端獲得數據。要模擬這個過程首先當然是建立服務器端應用,應用的形式沒有限制,你可以采用任何Web技術進行開發。假設現在有一個文件服務器,用戶輸入文件名稱就可以下載文件。服務器端程序很簡單,只要訪問http://192.168.1.208/FileDownload.aspx?file=filename,就可以下載指定filename的文件,由於服務器端開發的內容不是今天的重點在此不再贅述。客戶端界面設計如下圖:
程序的實現需要借助幾個對象:
NSURLRequest:建立了一個請求,可以指定緩存策略、超時時間。和NSURLRequest對應的還有一個NSMutableURLRequest,如果請求定義為NSMutableURLRequest則可以指定請求方法(GET或POST)等信息。
NSURLConnection:用於發送請求,可以指定請求和代理。當前調用NSURLConnection的start方法後開始發送異步請求。
程序代碼如下:
// // KCMainViewController.m // UrlConnection // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" @interface KCMainViewController (){ NSMutableData *_data;//響應數據 UITextField *_textField; UIButton *_button; UIProgressView *_progressView; UILabel *_label; long long _totalLength; } @end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark - 私有方法 #pragma mark 界面布局 -(void)layoutUI{ //地址欄 _textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)]; _textField.borderStyle=UITextBorderStyleRoundedRect; _textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; _textField.text=@"簡約至上:交互式設計四策略.epub"; [self.view addSubview:_textField]; //進度條 _progressView=[[UIProgressView alloc]initWithFrame:CGRectMake(10, 100, 300, 25)]; [self.view addSubview:_progressView]; //狀態顯示 _label=[[UILabel alloc]initWithFrame:CGRectMake(10, 130, 300, 25)]; _label.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; [self.view addSubview:_label]; //下載按鈕 _button=[[UIButton alloc]initWithFrame:CGRectMake(10, 500, 300, 25)]; [_button setTitle:@"下載" forState:UIControlStateNormal]; [_button setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_button addTarget:self action:@selector(sendRequest) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_button]; } #pragma mark 更新進度 -(void)updateProgress{ // [[NSOperationQueue mainQueue] addOperationWithBlock:^{ if (_data.length==_totalLength) { _label.text=@"下載完成"; }else{ _label.text=@"正在下載..."; [_progressView setProgress:(float)_data.length/_totalLength]; } // }]; } #pragma mark 發送數據請求 -(void)sendRequest{ NSString *urlStr=[NSString stringWithFormat:@"http://192.168.1.208/FileDownload.aspx?file=%@",_textField.text]; //注意對於url中的中文是無法解析的,需要進行url編碼(指定編碼類型為utf-8) //另外注意url解碼使用stringByRemovingPercentEncoding方法 urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; //創建url鏈接 NSURL *url=[NSURL URLWithString:urlStr]; /*創建請求 cachePolicy:緩存策略 a.NSURLRequestUseProtocolCachePolicy 協議緩存,根據response中的Cache-Control字段判斷緩存是否有效,如果緩存有效則使用緩存數據否則重新從服務器請求 b.NSURLRequestReloadIgnoringLocalCacheData 不使用緩存,直接請求新數據 c.NSURLRequestReloadIgnoringCacheData 等同於 SURLRequestReloadIgnoringLocalCacheData d.NSURLRequestReturnCacheDataElseLoad 直接使用緩存數據不管是否有效,沒有緩存則重新請求 eNSURLRequestReturnCacheDataDontLoad 直接使用緩存數據不管是否有效,沒有緩存數據則失敗 timeoutInterval:超時時間設置(默認60s) */ NSURLRequest *request=[[NSURLRequest alloc]initWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0f]; //創建連接 NSURLConnection *connection=[[NSURLConnection alloc]initWithRequest:request delegate:self]; //啟動連接 [connection start]; } #pragma mark - 連接代理方法 #pragma mark 開始響應 -(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ NSLog(@"receive response."); _data=[[NSMutableData alloc]init]; _progressView.progress=0; //通過響應頭中的Content-Length取得整個響應的總長度 NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSDictionary *httpResponseHeaderFields = [httpResponse allHeaderFields]; _totalLength = [[httpResponseHeaderFields objectForKey:@"Content-Length"] longLongValue]; } #pragma mark 接收響應數據(根據響應內容的大小此方法會被重復調用) -(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ NSLog(@"receive data."); //連續接收數據 [_data appendData:data]; //更新進度 [self updateProgress]; } #pragma mark 數據接收完成 -(void)connectionDidFinishLoading:(NSURLConnection *)connection{ NSLog(@"loading finish."); //數據接收完保存文件(注意蘋果官方要求:下載數據只能保存在緩存目錄) NSString *savePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; savePath=[savePath stringByAppendingPathComponent:_textField.text]; [_data writeToFile:savePath atomically:YES]; NSLog(@"path:%@",savePath); } #pragma mark 請求失敗 -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{ //如果連接超時或者連接地址錯誤可能就會報錯 NSLog(@"connection error,error detail is:%@",error.localizedDescription); } @end
運行效果:
需要注意:
當然,對於上面文件下載這種大數據響應的情況使用代理方法處理響應具有一定的優勢(可以獲得傳輸進度)。但是如果現響應數據不是文件而是一段字符串(注意web請求的數據可以是字符串或者二進制,上面文件下載示例中響應數據是二進制),那麼采用代理方法處理服務器響應就未免有些太麻煩了。其實蘋果官方已經提供了下面兩種方法處理一般的請求:
+ (void)sendAsynchronousRequest:request: queue:queue:completionHandler:發送一個異步請求
+ (NSData *)sendSynchronousRequest: returningResponse: error:發送一個同步請求
假設在開發一個類似於微博的應用,服務器端返回的是JSON字符串,我們可以使用上面的方法簡化整個請求響應的過程。這裡會使用在“iOS開發系列--UITableView全面解析”文章中自定義的UITableViewCell來顯示微博數據,不清楚的朋友可以看一下前面的內容。
請求過程中需要傳遞一個用戶名和密碼,如果全部正確則服務器端返回此用戶可以看到的最新微博數據,響應的json格式大致如下:
整個Json最外層是statuses節點,它是一個數組類型,數組中每個元素都是一條微博數據,每條微博數據中除了包含微博信息還包含了發表用戶的信息。
首先需要先定義用戶模型KCUser
// // KCUser.h // UrlConnection // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import@interface KCUser : NSObject #pragma mark 編號 @property (nonatomic,strong) NSNumber *Id; #pragma mark 用戶名 @property (nonatomic,copy) NSString *name; #pragma mark 城市 @property (nonatomic,copy) NSString *city; @end
微博模型KCStatus
KCStatus.h
// // KCStatus.h // UITableView // // Created by Kenshin Cui on 14-3-1. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import#import "KCUser.h" @interface KCStatus : NSObject #pragma mark - 屬性 @property (nonatomic,strong) NSNumber *Id;//微博id @property (nonatomic,copy) NSString *profileImageUrl;//頭像 @property (nonatomic,strong) KCUser *user;//發送用戶 @property (nonatomic,copy) NSString *mbtype;//會員類型 @property (nonatomic,copy) NSString *createdAt;//創建時間 @property (nonatomic,copy) NSString *source;//設備來源 @property (nonatomic,copy) NSString *text;//微博內容 @end
KCStatus.m
// // KCStatus.m // UITableView // // Created by Kenshin Cui on 14-3-1. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCStatus.h" @implementation KCStatus -(NSString *)source{ return [NSString stringWithFormat:@"來自 %@",_source]; } @end
其次需要自定義微博顯示的單元格KCStatusTableViewCell,這裡需要注意,由於服務器返回數據中頭像和會員類型圖片已經不在本地,需要從服務器端根據返回JSON的中圖片的路徑去加載。
KCStatusTableViewCell.h
// // KCStatusTableViewCell.h // UITableView // // Created by Kenshin Cui on 14-3-1. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import@class KCStatus; @interface KCStatusTableViewCell : UITableViewCell #pragma mark 微博對象 @property (nonatomic,strong) KCStatus *status; #pragma mark 單元格高度 @property (assign,nonatomic) CGFloat height; @end
KCStatusTableViewCell.m
// // KCStatusTableViewCell.m // UITableView // // Created by Kenshin Cui on 14-3-1. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCStatusTableViewCell.h" #import "KCStatus.h" #define KCColor(r,g,b) [UIColor colorWithHue:r/255.0 saturation:g/255.0 brightness:b/255.0 alpha:1] //顏色宏定義 #define kStatusTableViewCellControlSpacing 10 //控件間距 #define kStatusTableViewCellBackgroundColor KCColor(251,251,251) #define kStatusGrayColor KCColor(50,50,50) #define kStatusLightGrayColor KCColor(120,120,120) #define kStatusTableViewCellAvatarWidth 40 //頭像寬度 #define kStatusTableViewCellAvatarHeight kStatusTableViewCellAvatarWidth #define kStatusTableViewCellUserNameFontSize 14 #define kStatusTableViewCellMbTypeWidth 13 //會員圖標寬度 #define kStatusTableViewCellMbTypeHeight kStatusTableViewCellMbTypeWidth #define kStatusTableViewCellCreateAtFontSize 12 #define kStatusTableViewCellSourceFontSize 12 #define kStatusTableViewCellTextFontSize 14 @interface KCStatusTableViewCell(){ UIImageView *_avatar;//頭像 UIImageView *_mbType;//會員類型 UILabel *_userName; UILabel *_createAt; UILabel *_source; UILabel *_text; } @end @implementation KCStatusTableViewCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self initSubView]; } return self; } #pragma mark 初始化視圖 -(void)initSubView{ //頭像控件 _avatar=[[UIImageView alloc]init]; [self addSubview:_avatar]; //用戶名 _userName=[[UILabel alloc]init]; _userName.textColor=kStatusGrayColor; _userName.font=[UIFont systemFontOfSize:kStatusTableViewCellUserNameFontSize]; [self addSubview:_userName]; //會員類型 _mbType=[[UIImageView alloc]init]; [self addSubview:_mbType]; //日期 _createAt=[[UILabel alloc]init]; _createAt.textColor=kStatusLightGrayColor; _createAt.font=[UIFont systemFontOfSize:kStatusTableViewCellCreateAtFontSize]; [self addSubview:_createAt]; //設備 _source=[[UILabel alloc]init]; _source.textColor=kStatusLightGrayColor; _source.font=[UIFont systemFontOfSize:kStatusTableViewCellSourceFontSize]; [self addSubview:_source]; //內容 _text=[[UILabel alloc]init]; _text.textColor=kStatusGrayColor; _text.font=[UIFont systemFontOfSize:kStatusTableViewCellTextFontSize]; _text.numberOfLines=0; _text.lineBreakMode=NSLineBreakByWordWrapping; [self addSubview:_text]; } #pragma mark 設置微博 -(void)setStatus:(KCStatus *)status{ //設置頭像大小和位置 CGFloat avatarX=10,avatarY=10; CGRect avatarRect=CGRectMake(avatarX, avatarY, kStatusTableViewCellAvatarWidth, kStatusTableViewCellAvatarHeight); // _avatar.image=[UIImage imageNamed:status.profileImageUrl]; NSURL *avatarUrl=[NSURL URLWithString:status.profileImageUrl]; NSData *avatarData=[NSData dataWithContentsOfURL:avatarUrl]; UIImage *avatarImage= [UIImage imageWithData:avatarData]; _avatar.image=avatarImage; _avatar.frame=avatarRect; //設置會員圖標大小和位置 CGFloat userNameX= CGRectGetMaxX(_avatar.frame)+kStatusTableViewCellControlSpacing ; CGFloat userNameY=avatarY; //根據文本內容取得文本占用空間大小 CGSize userNameSize=[status.user.name sizeWithAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:kStatusTableViewCellUserNameFontSize]}]; CGRect userNameRect=CGRectMake(userNameX, userNameY, userNameSize.width,userNameSize.height); _userName.text=status.user.name; _userName.frame=userNameRect; //設置會員圖標大小和位置 CGFloat mbTypeX=CGRectGetMaxX(_userName.frame)+kStatusTableViewCellControlSpacing; CGFloat mbTypeY=avatarY; CGRect mbTypeRect=CGRectMake(mbTypeX, mbTypeY, kStatusTableViewCellMbTypeWidth, kStatusTableViewCellMbTypeHeight); // _mbType.image=[UIImage imageNamed:status.mbtype]; NSURL *mbTypeUrl=[NSURL URLWithString:status.mbtype]; NSData *mbTypeData=[NSData dataWithContentsOfURL:mbTypeUrl]; UIImage *mbTypeImage= [UIImage imageWithData:mbTypeData]; _mbType.image=mbTypeImage; _mbType.frame=mbTypeRect; //設置發布日期大小和位置 CGSize createAtSize=[status.createdAt sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:kStatusTableViewCellCreateAtFontSize]}]; CGFloat createAtX=userNameX; CGFloat createAtY=CGRectGetMaxY(_avatar.frame)-createAtSize.height; CGRect createAtRect=CGRectMake(createAtX, createAtY, createAtSize.width, createAtSize.height); _createAt.text=status.createdAt; _createAt.frame=createAtRect; //設置設備信息大小和位置 CGSize sourceSize=[status.source sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:kStatusTableViewCellSourceFontSize]}]; CGFloat sourceX=CGRectGetMaxX(_createAt.frame)+kStatusTableViewCellControlSpacing; CGFloat sourceY=createAtY; CGRect sourceRect=CGRectMake(sourceX, sourceY, sourceSize.width,sourceSize.height); _source.text=status.source; _source.frame=sourceRect; //設置微博內容大小和位置 CGFloat textX=avatarX; CGFloat textY=CGRectGetMaxY(_avatar.frame)+kStatusTableViewCellControlSpacing; CGFloat textWidth=self.frame.size.width-kStatusTableViewCellControlSpacing*2; CGSize textSize=[status.text boundingRectWithSize:CGSizeMake(textWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:kStatusTableViewCellTextFontSize]} context:nil].size; CGRect textRect=CGRectMake(textX, textY, textSize.width, textSize.height); _text.text=status.text; _text.frame=textRect; _height=CGRectGetMaxY(_text.frame)+kStatusTableViewCellControlSpacing; } #pragma mark 重寫選擇事件,取消選中 -(void)setSelected:(BOOL)selected animated:(BOOL)animated{ } @end
最後就是KCMainViewController,在這裡需要使用NSURLConnection的靜態方法發送請求、獲得請求數據,然後對請求數據進行JSON序列化,將JSON字符串序列化成微博對象通過UITableView顯示到界面中。
// // KCMainViewController.m // UrlConnection // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" #import "KCStatusTableViewCell.h" #import "KCStatus.h" #import "KCUser.h" #define kURL @"http://192.168.1.208/ViewStatus.aspx" @interface KCMainViewController (){ UITableView *_tableView; NSMutableArray *_status; NSMutableArray *_statusCells;//存儲cell,用於計算高度 NSString *_userName; NSString *_password; } @end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; _userName=@"KenshinCui"; _password=@"123"; [self layoutUI]; [self sendRequest]; } #pragma mark - 私有方法 #pragma mark 界面布局 -(void)layoutUI{ _tableView =[[UITableView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame style:UITableViewStylePlain]; _tableView.dataSource=self; _tableView.delegate=self; [self.view addSubview:_tableView]; } #pragma mark 加載數據 -(void)loadData:(NSData *)data{ _status=[[NSMutableArray alloc]init]; _statusCells=[[NSMutableArray alloc]init]; /*json序列化 options:序列化選項,枚舉類型,但是可以指定為枚舉以外的類型,例如指定為0則可以返回NSDictionary或者NSArray a.NSJSONReadingMutableContainers:返回NSMutableDictionary或NSMutableArray b.NSJSONReadingMutableLeaves:返回NSMutableString字符串 c.NSJSONReadingAllowFragments:可以解析JSON字符串的外層既不是字典類型(NSMutableDictionary、NSDictionary)又不是數組類型(NSMutableArray、NSArray)的數據,但是必須是有效的JSON字符串 error:錯誤信息 */ NSError *error; //將對象序列化為字典 NSDictionary *dic= [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; NSArray *array= (NSArray *)dic[@"statuses"]; [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { KCStatus *status=[[KCStatus alloc]init]; //通過KVC給對象賦值 [status setValuesForKeysWithDictionary:obj]; KCUser *user=[[KCUser alloc]init]; [user setValuesForKeysWithDictionary:obj[@"user"]]; status.user=user; [_status addObject:status]; //存儲tableViewCell KCStatusTableViewCell *cell=[[KCStatusTableViewCell alloc]init]; [_statusCells addObject:cell]; }]; } #pragma mark 發送數據請求 -(void)sendRequest{ NSString *urlStr=[NSString stringWithFormat:@"%@",kURL]; //注意對於url中的中文是無法解析的,需要進行url編碼(指定編碼類型位utf-8) //另外注意url解碼使用stringByRemovingPercentEncoding方法 urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; //創建url鏈接 NSURL *url=[NSURL URLWithString:urlStr]; /*創建可變請求*/ NSMutableURLRequest *requestM=[[NSMutableURLRequest alloc]initWithURL:url cachePolicy:0 timeoutInterval:5.0f]; [requestM setHTTPMethod:@"POST"];//設置位post請求 //創建post參數 NSString *bodyDataStr=[NSString stringWithFormat:@"userName=%@&password=%@",_userName,_password]; NSData *bodyData=[bodyDataStr dataUsingEncoding:NSUTF8StringEncoding]; [requestM setHTTPBody:bodyData]; //發送一個異步請求 [NSURLConnection sendAsynchronousRequest:requestM queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { if (!connectionError) { // NSString *jsonStr=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; // NSLog(@"jsonStr:%@",jsonStr); //加載數據 [self loadData:data]; //刷新表格 [_tableView reloadData]; }else{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ }]; } }]; } #pragma mark - 數據源方法 #pragma mark 返回分組數 -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{ return 1; } #pragma mark 返回每組行數 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return _status.count; } #pragma mark返回每行的單元格 -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ static NSString *cellIdentifier=@"UITableViewCellIdentifierKey1"; KCStatusTableViewCell *cell; cell=[tableView dequeueReusableCellWithIdentifier:cellIdentifier]; if(!cell){ cell=[[KCStatusTableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } //在此設置微博,以便重新布局 KCStatus *status=_status[indexPath.row]; cell.status=status; return cell; } #pragma mark - 代理方法 #pragma mark 重新設置單元格高度 -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ KCStatusTableViewCell *cell= _statusCells[indexPath.row]; cell.status=_status[indexPath.row]; return cell.height; } @end
運行效果:
可以看到使用NSURLConnection封裝的靜態方法可以直接獲得NSData,不需要使用代理一步步自己組裝數據。這裡采用了POST方式發送請求,使用POST發送請求需要組裝數據體,不過數據長度不像GET方式存在限制。從iOS5開始蘋果官方提供了JSON序列化和反序列化相關方法(上面程序中僅僅用到了反序列化方法,序列化使用dataWithJSONObject:options:opt error:方法)方便的對數組和字典進行序列化和反序列化。但是注意反序列化參數設置,程序中設置成了0,直接反序列化為不可變對象以提高性能。
注意:
1.現在多數情況下互聯網數據都是以JSON格式進行傳輸,但是有時候也會面對XML存儲。在IOS中可以使用NSXMLParser進行XML解析,由於實際使用並不多,在此不再贅述。
2.使用KVC給對象賦值時(通常是NSDictionary或NSMutalbeDictionary)注意對象的屬性最好不要定義為基本類型(如int),否則如果屬性值為null則會報錯,最後定義為ObjC對象類型(如使用NSNumber代替int等);
開發Web類的應用圖片緩存問題不得不提及,因為圖片的下載相當耗時。對於前面的微博數據,頭像和微博類型圖標在數據庫中是以鏈接形式存放的,取得鏈接後還必須進行對應的圖片加載。大家都知道圖片往往要比文本內容大得多,在UITableView中上下滾動就會重新加載數據,對於文本由於已經加載到本地自然不存在問題,但是對於圖片來說如果每次都必須重新從服務器端加載就不太合適了。
解決圖片加載的辦法有很多,可以事先存儲到內存中,也可以保存到臨時文件。在內存中存儲雖然簡單但是往往不可取,因為程序重新啟動之後還面臨這重新請求的問題,類似於新浪微博、QQ、微信等應用一般會存儲在文件中,這樣應用程序即使重啟也會從文件中讀取。但是使用文件緩存圖片可能就要自己做很多事情,例如緩存文件是否過期?緩存數據越來越大如何管理存儲空間?
這些問題其實很多第三方框架已經做的很好了,實際開發中往往會采用一些第三方框架來處理圖片。例如這裡可以選用SDWebImage框架。SDWebImage使用起來相當簡單,開發者不必過多關心它的緩存和多線程加載問題,一個方法就可以解決。這裡直接修改KCStatusTableViewCell中相關代碼即可:
#pragma mark 設置微博 -(void)setStatus:(KCStatus *)status{ //設置頭像大小和位置 CGFloat avatarX=10,avatarY=10; CGRect avatarRect=CGRectMake(avatarX, avatarY, kStatusTableViewCellAvatarWidth, kStatusTableViewCellAvatarHeight); NSURL *avatarUrl=[NSURL URLWithString:status.profileImageUrl]; UIImage *defaultAvatar=[UIImage imageNamed:@"defaultAvatar.jpg"];//默認頭像 [_avatar sd_setImageWithURL:avatarUrl placeholderImage:defaultAvatar]; _avatar.frame=avatarRect; //設置會員圖標大小和位置 CGFloat userNameX= CGRectGetMaxX(_avatar.frame)+kStatusTableViewCellControlSpacing ; CGFloat userNameY=avatarY; //根據文本內容取得文本占用空間大小 CGSize userNameSize=[status.user.name sizeWithAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:kStatusTableViewCellUserNameFontSize]}]; CGRect userNameRect=CGRectMake(userNameX, userNameY, userNameSize.width,userNameSize.height); _userName.text=status.user.name; _userName.frame=userNameRect; //設置會員圖標大小和位置 CGFloat mbTypeX=CGRectGetMaxX(_userName.frame)+kStatusTableViewCellControlSpacing; CGFloat mbTypeY=avatarY; CGRect mbTypeRect=CGRectMake(mbTypeX, mbTypeY, kStatusTableViewCellMbTypeWidth, kStatusTableViewCellMbTypeHeight); NSURL *mbTypeUrl=[NSURL URLWithString:status.mbtype]; [_mbType sd_setImageWithURL:mbTypeUrl ]; _mbType.frame=mbTypeRect; //設置發布日期大小和位置 CGSize createAtSize=[status.createdAt sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:kStatusTableViewCellCreateAtFontSize]}]; CGFloat createAtX=userNameX; CGFloat createAtY=CGRectGetMaxY(_avatar.frame)-createAtSize.height; CGRect createAtRect=CGRectMake(createAtX, createAtY, createAtSize.width, createAtSize.height); _createAt.text=status.createdAt; _createAt.frame=createAtRect; //設置設備信息大小和位置 CGSize sourceSize=[status.source sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:kStatusTableViewCellSourceFontSize]}]; CGFloat sourceX=CGRectGetMaxX(_createAt.frame)+kStatusTableViewCellControlSpacing; CGFloat sourceY=createAtY; CGRect sourceRect=CGRectMake(sourceX, sourceY, sourceSize.width,sourceSize.height); _source.text=status.source; _source.frame=sourceRect; //設置微博內容大小和位置 CGFloat textX=avatarX; CGFloat textY=CGRectGetMaxY(_avatar.frame)+kStatusTableViewCellControlSpacing; CGFloat textWidth=self.frame.size.width-kStatusTableViewCellControlSpacing*2; CGSize textSize=[status.text boundingRectWithSize:CGSizeMake(textWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:kStatusTableViewCellTextFontSize]} context:nil].size; CGRect textRect=CGRectMake(textX, textY, textSize.width, textSize.height); _text.text=status.text; _text.frame=textRect; _height=CGRectGetMaxY(_text.frame)+kStatusTableViewCellControlSpacing; }
運行效果:
在上面的方法中直接調用了SDWebImage的分類緩存方法設置圖片,這個方法可以分配另外一個線程去加載圖片(同時對於頭像還指定了默認圖片,網速較慢時不至於顯示空白),圖片加載後存放在沙箱的緩存文件夾,如下圖:
滾動UITableView再次加載同一個圖片時SDWebImage就會自動判斷緩存文件是否有效,如果有效就加載緩存文件,否則重新加載。SDWebImage有很多使用的方法,感興趣的朋友可以訪問“SDWebImage Reference)”。
通過前面的演示大家應該對於iOS的Web請求有了大致的了解,可以通過代理方法接收數據也可以直接通過靜態方法接收數據,但是實際開發中更推薦使用靜態方法。關於前面的文件下載示例,更多的是希望大家了解代理方法接收響應數據的過程,實際開發中也不可能使用這種方法進行文件下載。這種下載有個致命的問題:不適合進行大文件分段下載。因為代理方法在接收數據時雖然表面看起來是每次讀取一部分響應數據,事實上它只有一次請求並且也只接收了一次服務器響應,只是當響應數據較大時系統會重復調用數據接收方法,每次將已讀取的數據拿出一部分交給數據接收方法。這樣一來對於上G的文件進行下載,如果中途暫停的話再次請求還是從頭開始下載,不適合大文件斷點續傳(另外說明一點,上面NSURLConnection示例中使用了NSMutableData進行數據接收和追加只是為了方便演示,實際開發建議直接寫入文件)。
實際開發文件下載的時候不管是通過代理方法還是靜態方法執行請求和響應,我們都會分批請求數據,而不是一次性請求數據。假設一個文件有1G,那麼只要每次請求1M的數據,請求1024次也就下載完了。那麼如何讓服務器每次只返回1M的數據呢?
在網絡開發中可以在請求的頭文件中設置一個range信息,它代表請求數據的大小。通過這個字段配合服務器端可以精確的控制每次服務器響應的數據范圍。例如指定bytes=0-1023,然後在服務器端解析Range信息,返回該文件的0到1023之間的數據的數據即可(共1024Byte)。這樣,只要在每次發送請求控制這個頭文件信息就可以做到分批請求。
當然,為了讓整個數據保持完整,每次請求的數據都需要逐步追加直到整個文件請求完成。但是如何知道整個文件的大小?其實在前面的文件下載演示中大家可以看到,可以通過頭文件信息獲取整個文件大小。但是這麼做的話就必須請求整個數據,這樣分段下載就沒有任何意義了。所幸在WEB開發中我們還有另一種請求方法“HEAD”,通過這種請求服務器只會響應頭信息,其他數據不會返回給客戶端,這樣一來整個數據的大小也就可以得到了。下面給出完整的程序代碼,關鍵的地方已經給出注釋(為了簡化代碼,這裡沒有使用代理方法):
KCMainViewController.m
// // KCMainViewController.m // UrlConnection // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" #define kUrl @"http://192.168.1.208/FileDownload.aspx" #define kFILE_BLOCK_SIZE (1024) //每次1KB @interface KCMainViewController (){ UITextField *_textField; UIButton *_button; UIProgressView *_progressView; UILabel *_label; long long _totalLength; long long _loadedLength; } @end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark - 私有方法 #pragma mark 界面布局 -(void)layoutUI{ //地址欄 _textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)]; _textField.borderStyle=UITextBorderStyleRoundedRect; _textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; _textField.text=@"1.jpg"; // _textField.text=@"1.jpg"; [self.view addSubview:_textField]; //進度條 _progressView=[[UIProgressView alloc]initWithFrame:CGRectMake(10, 100, 300, 25)]; [self.view addSubview:_progressView]; //狀態顯示 _label=[[UILabel alloc]initWithFrame:CGRectMake(10, 130, 300, 25)]; _label.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; [self.view addSubview:_label]; //下載按鈕 _button=[[UIButton alloc]initWithFrame:CGRectMake(10, 500, 300, 25)]; [_button setTitle:@"下載" forState:UIControlStateNormal]; [_button setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_button addTarget:self action:@selector(downloadFileAsync) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_button]; } #pragma mark 更新進度 -(void)updateProgress{ [[NSOperationQueue mainQueue] addOperationWithBlock:^{ if (_loadedLength==_totalLength) { _label.text=@"下載完成"; }else{ _label.text=@"正在下載..."; } [_progressView setProgress:(double)_loadedLength/_totalLength]; }]; } #pragma mark 取得請求鏈接 -(NSURL *)getDownloadUrl:(NSString *)fileName{ NSString *urlStr=[NSString stringWithFormat:@"%@?file=%@",kUrl,fileName]; urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; return url; } #pragma mark 取得保存地址(保存在沙盒緩存目錄) -(NSString *)getSavePath:(NSString *)fileName{ NSString *path=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; return [path stringByAppendingPathComponent:fileName]; } #pragma mark 文件追加 -(void)fileAppend:(NSString *)filePath data:(NSData *)data{ //以可寫方式打開文件 NSFileHandle *fileHandle=[NSFileHandle fileHandleForWritingAtPath:filePath]; //如果存在文件則追加,否則創建 if (fileHandle) { [fileHandle seekToEndOfFile]; [fileHandle writeData:data]; [fileHandle closeFile];//關閉文件 }else{ [data writeToFile:filePath atomically:YES];//創建文件 } } #pragma mark 取得文件大小 -(long long)getFileTotlaLength:(NSString *)fileName{ NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[self getDownloadUrl:fileName] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0f]; //設置為頭信息請求 [request setHTTPMethod:@"HEAD"]; NSURLResponse *response; NSError *error; //注意這裡使用了同步請求,直接將文件大小返回 [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; if (error) { NSLog(@"detail error:%@",error.localizedDescription); } //取得內容長度 return response.expectedContentLength; } #pragma mark 下載指定塊大小的數據 -(void)downloadFile:(NSString *)fileName startByte:(long long)start endByte:(long long)end{ NSString *range=[NSString stringWithFormat:@"Bytes=%lld-%lld",start,end]; NSLog(@"%@",range); // NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[self getDownloadUrl:fileName]]; NSMutableURLRequest *request= [NSMutableURLRequest requestWithURL:[self getDownloadUrl:fileName] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0f]; //通過請求頭設置數據請求范圍 [request setValue:range forHTTPHeaderField:@"Range"]; NSURLResponse *response; NSError *error; //注意這裡使用同步請求,避免文件塊追加順序錯誤 NSData *data= [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; if(!error){ NSLog(@"dataLength=%lld",(long long)data.length); [self fileAppend:[self getSavePath:fileName] data:data]; } else{ NSLog(@"detail error:%@",error.localizedDescription); } } #pragma mark 文件下載 -(void)downloadFile{ _totalLength=[self getFileTotlaLength:_textField.text]; _loadedLength=0; long long startSize=0; long long endSize=0; //分段下載 while(startSize< _totalLength){ endSize=startSize+kFILE_BLOCK_SIZE-1; if (endSize>_totalLength) { endSize=_totalLength-1; } [self downloadFile:_textField.text startByte:startSize endByte:endSize]; //更新進度 _loadedLength+=(endSize-startSize)+1; [self updateProgress]; startSize+=kFILE_BLOCK_SIZE; } } #pragma mark 異步下載文件 -(void)downloadFileAsync{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self downloadFile]; }); } @end
運行效果:
下載文件的生成過程:
分段下載的過程實現並不復雜,主要是需要配合後台進行響應進行操作。針對不同的開發技術,服務器端處理方式稍有差別,但是基本原理是一樣的,那就是讀取Range信息,按需提供相應數據。
在做WEB應用程序開發時,如果要上傳一個文件往往會給form設置一個enctype=”multipart/form-data”的屬性,不設置這個值在後台無法正常接收文件。在WEB開發過程中,form的這個屬性其實本質就是指定請求頭中Content-Type類型,當然使用GET方法提交就不用說了,必須使用URL編碼。但是如果使用POST方法傳遞數據其實也是類似的,同樣需要進行編碼,具體編碼方式其實就是通過enctype屬性進行設置的。常用的屬性值有:
要實現文件上傳,必須采用POST上傳,同時請求類型必須是multipart/form-data。在Web開發中,開發人員不必過多的考慮mutiparty/form-data更多的細節,一般使用file控件即可完成文件上傳。但是在iOS中如果要實現文件上傳,就沒有那麼簡單了,我們必須了解這種數據類型的請求是如何工作的。
下面是在浏覽器中上傳一個文件時,發送的請求頭:
這是發送的請求體內容:
在請求頭中,最重要的就是Content-Type,它的值分為兩部分:前半部分是內容類型,前面已經解釋過了;後面是邊界boundary用來分隔表單中不同部分的數據,後面一串數字是浏覽器自動生成的,它的格式並不固定,可以是任意字符。和請求體中的源代碼部分進行對比不難發現其實boundary的內容和請求體的數據部分前的字符串相比少了兩個“--”。請求體中Content-Disposition中指定了表單元素的name屬性和文件名稱,同時指定了Content-Type表示文件類型。當然,在請求體中最重要的就是後面的數據部分,它其實就是二進制字符串。由此可以得出以下結論,請求體內容由如下幾部分按順序執行組成:
--boundary Content-Disposition:form-data;name=”表單控件名稱”;filename=”上傳文件名稱” Content-Type:文件MIME Types 文件二進制數據; --boundary--
了解這些信息後,只要使用POST方法給服務器端發送請求並且請求內容按照上面的格式設置即可。
下面是實現代碼:
// // KCMainViewController.m // UrlConnection // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" #define kUrl @"http://192.168.1.208/FileUpload.aspx" #define kBOUNDARY_STRING @"KenshinCui" @interface KCMainViewController (){ UITextField *_textField; UIButton *_button; } @end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark - 私有方法 #pragma mark 界面布局 -(void)layoutUI{ //地址欄 _textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)]; _textField.borderStyle=UITextBorderStyleRoundedRect; _textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; _textField.text=@"pic.jpg"; [self.view addSubview:_textField]; //上傳按鈕 _button=[[UIButton alloc]initWithFrame:CGRectMake(10, 500, 300, 25)]; [_button setTitle:@"上傳" forState:UIControlStateNormal]; [_button setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_button addTarget:self action:@selector(uploadFile) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_button]; } #pragma mark 取得請求鏈接 -(NSURL *)getUploadUrl:(NSString *)fileName{ NSString *urlStr=[NSString stringWithFormat:@"%@?file=%@",kUrl,fileName]; urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; return url; } #pragma mark 取得mime types -(NSString *)getMIMETypes:(NSString *)fileName{ return @"image/jpg"; } #pragma mark 取得數據體 -(NSData *)getHttpBody:(NSString *)fileName{ NSMutableData *dataM=[NSMutableData data]; NSString *strTop=[NSString stringWithFormat:@"--%@\nContent-Disposition: form-data; name=\"file1\"; filename=\"%@\"\nContent-Type: %@\n\n",kBOUNDARY_STRING,fileName,[self getMIMETypes:fileName]]; NSString *strBottom=[NSString stringWithFormat:@"\n--%@--",kBOUNDARY_STRING]; NSString *filePath=[[NSBundle mainBundle] pathForResource:fileName ofType:nil]; NSData *fileData=[NSData dataWithContentsOfFile:filePath]; [dataM appendData:[strTop dataUsingEncoding:NSUTF8StringEncoding]]; [dataM appendData:fileData]; [dataM appendData:[strBottom dataUsingEncoding:NSUTF8StringEncoding]]; return dataM; } #pragma mark 上傳文件 -(void)uploadFile{ NSString *fileName=_textField.text; NSMutableURLRequest *request= [NSMutableURLRequest requestWithURL:[self getUploadUrl:fileName] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0f]; request.HTTPMethod=@"POST"; NSData *data=[self getHttpBody:fileName]; //通過請求頭設置 [request setValue:[NSString stringWithFormat:@"%lu",(unsigned long)data.length] forHTTPHeaderField:@"Content-Length"]; [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",kBOUNDARY_STRING] forHTTPHeaderField:@"Content-Type"]; //設置數據體 request.HTTPBody=data; //發送請求 [NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc]init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { if(connectionError){ NSLog(@"error:%@",connectionError.localizedDescription); } }]; } @end
NSURLConnection是2003年伴隨著Safari一起發行的網絡開發API,距今已經有十一年。當然,在這十一年間它表現的相當優秀,有大量的應用基礎,這也是為什麼前面花了那麼長時間對它進行詳細介紹的原因。但是這些年伴隨著iPhone、iPad的發展,對於NSURLConnection設計理念也提出了新的挑戰。在2013年WWDC上蘋果揭開了NSURLSession的面紗,將它作為NSURLConnection的繼任者。相比較NSURLConnection,NSURLSession提供了配置會話緩存、協議、cookie和證書能力,這使得網絡架構和應用程序可以獨立工作、互不干擾。另外,NSURLSession另一個重要的部分是會話任務,它負責加載數據,在客戶端和服務器端進行文件的上傳下載。
通過前面的介紹大家可以看到,NSURLConnection完成的三個主要任務:獲取數據(通常是JSON、XML等)、文件上傳、文件下載。其實在NSURLSession時代,他們分別由三個任務來完成:NSURLSessionData、NSURLSessionUploadTask、NSURLSessionDownloadTask,這三個類都是NSURLSessionTask這個抽象類的子類,相比直接使用NSURLConnection,NSURLSessionTask支持任務的暫停、取消和恢復,並且默認任務運行在其他非主線程中,具體關系圖如下:
前面通過請求一個微博數據進行數據請求演示,現在通過NSURLSessionDataTask實現這個功能,其實現流程與使用NSURLConnection的靜態方法類似,下面是主要代碼:
-(void)loadJsonData{ //1.創建url NSString *urlStr=[NSString stringWithFormat:@"http://192.168.1.208/ViewStatus.aspx?userName=%@&password=%@",@"KenshinCui",@"123"]; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; //2.創建請求 NSURLRequest *request=[NSURLRequest requestWithURL:url]; //3.創建會話(這裡使用了一個全局會話)並且啟動任務 NSURLSession *session=[NSURLSession sharedSession]; //從會話創建任務 NSURLSessionDataTask *dataTask=[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error) { NSString *dataStr=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"%@",dataStr); }else{ NSLog(@"error is :%@",error.localizedDescription); } }]; [dataTask resume];//恢復線程,啟動任務 }
下面看一下如何使用NSURLSessionUploadTask實現文件上傳,這裡貼出主要的幾個方法:
#pragma mark 取得mime types -(NSString *)getMIMETypes:(NSString *)fileName{ return @"image/jpg"; } #pragma mark 取得數據體 -(NSData *)getHttpBody:(NSString *)fileName{ NSString *boundary=@"KenshinCui"; NSMutableData *dataM=[NSMutableData data]; NSString *strTop=[NSString stringWithFormat:@"--%@\nContent-Disposition: form-data; name=\"file1\"; filename=\"%@\"\nContent-Type: %@\n\n",boundary,fileName,[self getMIMETypes:fileName]]; NSString *strBottom=[NSString stringWithFormat:@"\n--%@--",boundary]; NSString *filePath=[[NSBundle mainBundle] pathForResource:fileName ofType:nil]; NSData *fileData=[NSData dataWithContentsOfFile:filePath]; [dataM appendData:[strTop dataUsingEncoding:NSUTF8StringEncoding]]; [dataM appendData:fileData]; [dataM appendData:[strBottom dataUsingEncoding:NSUTF8StringEncoding]]; return dataM; } #pragma mark 上傳文件 -(void)uploadFile{ NSString *fileName=@"pic.jpg"; //1.創建url NSString *urlStr=@"http://192.168.1.208/FileUpload.aspx"; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; //2.創建請求 NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url]; request.HTTPMethod=@"POST"; //3.構建數據 NSString *path=[[NSBundle mainBundle] pathForResource:fileName ofType:nil]; NSData *data=[self getHttpBody:fileName]; request.HTTPBody=data; [request setValue:[NSString stringWithFormat:@"%lu",(unsigned long)data.length] forHTTPHeaderField:@"Content-Length"]; [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",@"KenshinCui"] forHTTPHeaderField:@"Content-Type"]; //4.創建會話 NSURLSession *session=[NSURLSession sharedSession]; NSURLSessionUploadTask *uploadTask=[session uploadTaskWithRequest:request fromData:data completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error) { NSString *dataStr=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"%@",dataStr); }else{ NSLog(@"error is :%@",error.localizedDescription); } }]; [uploadTask resume]; }
如果僅僅通過上面的方法或許文件上傳還看不出和NSURLConnection之間的區別,因為拼接上傳數據的過程和前面是一樣的。事實上在NSURLSessionUploadTask中還提供了一個- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler方法用於文件上傳。這個方法通常會配合“PUT”請求進行使用,由於PUT方法包含在Web DAV協議中,不同的WEB服務器其配置啟用PUT的方法也不同,並且出於安全考慮,各類WEB服務器默認對PUT請求也是拒絕的,所以實際使用時還需做重分考慮,在這裡不具體介紹,有興趣的朋友可以自己試驗一下。
使用NSURLSessionDownloadTask下載文件的過程與前面差不多,需要注意的是文件下載文件之後會自動保存到一個臨時目錄,需要開發人員自己將此文件重新放到其他指定的目錄中。
-(void)downloadFile{ //1.創建url NSString *fileName=@"1.jpg"; NSString *urlStr=[NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName]; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; //2.創建請求 NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url]; //3.創建會話(這裡使用了一個全局會話)並且啟動任務 NSURLSession *session=[NSURLSession sharedSession]; NSURLSessionDownloadTask *downloadTask=[session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { if (!error) { //注意location是下載後的臨時保存路徑,需要將它移動到需要保存的位置 NSError *saveError; NSString *cachePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *savePath=[cachePath stringByAppendingPathComponent:fileName]; NSLog(@"%@",savePath); NSURL *saveUrl=[NSURL fileURLWithPath:savePath]; [[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&saveError]; if (!saveError) { NSLog(@"save sucess."); }else{ NSLog(@"error is :%@",saveError.localizedDescription); } }else{ NSLog(@"error is :%@",error.localizedDescription); } }]; [downloadTask resume]; }
NSURLConnection通過全局狀態來管理cookies、認證信息等公共資源,這樣如果遇到兩個連接需要使用不同的資源配置情況時就無法解決了,但是這個問題在NSURLSession中得到了解決。NSURLSession同時對應著多個連接,會話通過工廠方法來創建,同一個會話中使用相同的狀態信息。NSURLSession支持進程三種會話:
defaultSessionConfiguration
:進程內會話(默認會話),用硬盤來緩存數據。ephemeralSessionConfiguration
:臨時的進程內會話(內存),不會將cookie、緩存儲存到本地,只會放到內存中,當應用程序退出後數據也會消失。backgroundSessionConfiguration
:後台會話,相比默認會話,該會話會在後台開啟一個線程進行網絡數據處理。下面將通過一個文件下載功能對兩種會話進行演示,在這個過程中也會用到任務的代理方法對上傳操作進行更加細致的控制。下面先看一下使用默認會話下載文件,代碼中演示了如何通過NSURLSessionConfiguration進行會話配置,如果通過代理方法進行文件下載進度展示(類似於前面中使用NSURLConnection代理方法,其實下載並未分段,如果需要分段需要配合後台進行),同時在這個過程中可以准確控制任務的取消、掛起和恢復。
// // KCMainViewController.m // URLSession // // Created by Kenshin Cui on 14-03-23. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" @interface KCMainViewController (){ UITextField *_textField; UIProgressView *_progressView; UILabel *_label; UIButton *_btnDownload; UIButton *_btnCancel; UIButton *_btnSuspend; UIButton *_btnResume; NSURLSessionDownloadTask *_downloadTask; } @end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark 界面布局 -(void)layoutUI{ //地址欄 _textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)]; _textField.borderStyle=UITextBorderStyleRoundedRect; _textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; _textField.text=@"[Objective-C.程序設計(第4版)].(斯蒂芬).林冀等.掃描版[電子書www.minxue.net].pdf"; [self.view addSubview:_textField]; //進度條 _progressView=[[UIProgressView alloc]initWithFrame:CGRectMake(10, 100, 300, 25)]; [self.view addSubview:_progressView]; //狀態顯示 _label=[[UILabel alloc]initWithFrame:CGRectMake(10, 130, 300, 25)]; _label.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; [self.view addSubview:_label]; //下載按鈕 _btnDownload=[[UIButton alloc]initWithFrame:CGRectMake(20, 500, 50, 25)]; [_btnDownload setTitle:@"下載" forState:UIControlStateNormal]; [_btnDownload setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_btnDownload addTarget:self action:@selector(downloadFile) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_btnDownload]; //取消按鈕 _btnCancel=[[UIButton alloc]initWithFrame:CGRectMake(100, 500, 50, 25)]; [_btnCancel setTitle:@"取消" forState:UIControlStateNormal]; [_btnCancel setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_btnCancel addTarget:self action:@selector(cancelDownload) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_btnCancel]; //掛起按鈕 _btnSuspend=[[UIButton alloc]initWithFrame:CGRectMake(180, 500, 50, 25)]; [_btnSuspend setTitle:@"掛起" forState:UIControlStateNormal]; [_btnSuspend setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_btnSuspend addTarget:self action:@selector(suspendDownload) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_btnSuspend]; //恢復按鈕 _btnResume=[[UIButton alloc]initWithFrame:CGRectMake(260, 500, 50, 25)]; [_btnResume setTitle:@"恢復" forState:UIControlStateNormal]; [_btnResume setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_btnResume addTarget:self action:@selector(resumeDownload) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_btnResume]; } #pragma mark 設置界面狀態 -(void)setUIStatus:(int64_t)totalBytesWritten expectedToWrite:(int64_t)totalBytesExpectedToWrite{ dispatch_async(dispatch_get_main_queue(), ^{ _progressView.progress=(float)totalBytesWritten/totalBytesExpectedToWrite; if (totalBytesWritten==totalBytesExpectedToWrite) { _label.text=@"下載完成"; [UIApplication sharedApplication].networkActivityIndicatorVisible=NO; _btnDownload.enabled=YES; }else{ _label.text=@"正在下載..."; [UIApplication sharedApplication].networkActivityIndicatorVisible=YES; } }); } #pragma mark 文件下載 -(void)downloadFile{ //1.創建url NSString *fileName=_textField.text; NSString *urlStr=[NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName]; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; //2.創建請求 NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url]; //3.創建會話 //默認會話 NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfig.timeoutIntervalForRequest=5.0f;//請求超時時間 sessionConfig.allowsCellularAccess=true;//是否允許蜂窩網絡下載(2G/3G/4G) //創建會話 NSURLSession *session=[NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];//指定配置和代理 _downloadTask=[session downloadTaskWithRequest:request]; [_downloadTask resume]; } #pragma mark 取消下載 -(void)cancelDownload{ [_downloadTask cancel]; } #pragma mark 掛起下載 -(void)suspendDownload{ [_downloadTask suspend]; } #pragma mark 恢復下載下載 -(void)resumeDownload{ [_downloadTask resume]; } #pragma mark - 下載任務代理 #pragma mark 下載中(會多次調用,可以記錄下載進度) -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ [self setUIStatus:totalBytesWritten expectedToWrite:totalBytesExpectedToWrite]; } #pragma mark 下載完成 -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{ NSError *error; NSString *cachePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *savePath=[cachePath stringByAppendingPathComponent:_textField.text]; NSLog(@"%@",savePath); NSURL *saveUrl=[NSURL fileURLWithPath:savePath]; [[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&error]; if (error) { NSLog(@"Error is:%@",error.localizedDescription); } } #pragma mark 任務完成,不管是否下載成功 -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{ [self setUIStatus:0 expectedToWrite:0]; if (error) { NSLog(@"Error is:%@",error.localizedDescription); } } @end
演示效果:
NSURLSession支持程序的後台下載和上傳,蘋果官方將其稱為進程之外的上傳和下載,這些任務都是交給後台守護線程完成的,而非應用程序本身。即使文件在下載和上傳過程中崩潰了也可以繼續運行(注意如果用戶強制退關閉應用程序,NSURLSession會斷開連接)。下面看一下如何在後台進行文件下載,這在實際開發中往往很有效,例如在手機上緩存一個視頻在沒有網絡的時候觀看(為了簡化程序這裡不再演示任務的取消、掛起等操作)。下面對前面的程序稍作調整使程序能在後台完成下載操作:
// // KCMainViewController.m // URLSession // // Created by Kenshin Cui on 14-03-23. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" #import "AppDelegate.h" @interface KCMainViewController (){ NSURLSessionDownloadTask *_downloadTask; NSString *_fileName; } @end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; [self downloadFile]; } #pragma mark 取得一個後台會話(保證一個後台會話,這通常很有必要) -(NSURLSession *)backgroundSession{ static NSURLSession *session; static dispatch_once_t token; dispatch_once(&token, ^{ NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.cmjstudio.URLSession"]; sessionConfig.timeoutIntervalForRequest=5.0f;//請求超時時間 sessionConfig.discretionary=YES;//系統自動選擇最佳網絡下載 sessionConfig.HTTPMaximumConnectionsPerHost=5;//限制每次最多一個連接 //創建會話 session=[NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];//指定配置和代理 }); return session; } #pragma mark 文件下載 -(void)downloadFile{ _fileName=@"1.mp4"; NSString *urlStr=[NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",_fileName]; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url]; //後台會話 _downloadTask=[[self backgroundSession] downloadTaskWithRequest:request]; [_downloadTask resume]; } #pragma mark - 下載任務代理 #pragma mark 下載中(會多次調用,可以記錄下載進度) -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ // [NSThread sleepForTimeInterval:0.5]; // NSLog(@"%.2f",(double)totalBytesWritten/totalBytesExpectedToWrite); } #pragma mark 下載完成 -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{ NSError *error; NSString *cachePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *savePath=[cachePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@",[NSDate date]]]; NSLog(@"%@",savePath); NSURL *saveUrl=[NSURL fileURLWithPath:savePath]; [[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&error]; if (error) { NSLog(@"didFinishDownloadingToURL:Error is %@",error.localizedDescription); } } #pragma mark 任務完成,不管是否下載成功 -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{ if (error) { NSLog(@"DidCompleteWithError:Error is %@",error.localizedDescription); } } @end
運行上面的程序會發現即使程序退出到後台也能正常完成文件下載。為了提高用戶體驗,通常會在下載時設置文件下載進度,但是通過前面的介紹可以知道:當程序進入後台後,事實上任務是交給iOS系統來調度的,具體什麼時候下載完成就不得而知,例如有個較大的文件經過一個小時下載完了,正常打開應用程序看到的此文件下載進度應該在100%的位置,但是由於程序已經在後台無法更新程序UI,而此時可以通過應用程序代理方法進行UI更新。具體原理如下圖所示:
當NSURLSession在後台開啟幾個任務之後,如果有其中幾個任務完成後系統會調用此應用程序的-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler代理方法;此方法會包含一個competionHandler(此操作表示應用完成所有處理工作),通常我們會保存此對象;直到最後一個任務完成,此時會重新通過會話標識(上面sessionConfig中設置的)找到對應的會話並調用NSURLSession的-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session代理方法,在這個方法中通常可以進行UI更新,並調用completionHandler通知系統已經完成所有操作。具體兩個方法代碼示例如下:
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{ //backgroundSessionCompletionHandler是自定義的一個屬性 self.backgroundSessionCompletionHandler=completionHandler; } -(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{ AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; //Other Operation.... if (appDelegate.backgroundSessionCompletionHandler) { void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler; appDelegate.backgroundSessionCompletionHandler = nil; completionHandler(); } }
網絡開發中還有一個常用的UI控件UIWebView,它是iOS中內置的浏覽器控件,功能十分強大。如一些社交軟件往往在應用程序內不需要打開其他浏覽器就能看一些新聞之類的頁面,就是通過這個控件實現的。需要注意的是UIWebView不僅能加載網絡資源還可以加載本地資源,目前支持的常用的文檔格式如:html、pdf、docx、txt等。
下面將通過一個UIWebView開發一個簡單的浏覽器,界面布局大致如下:
在這個浏覽器中將實現這樣幾個功能:
1.如果輸入以”file://”開頭的地址將加載Bundle中的文件
2.如果輸入以“http”開頭的地址將加載網絡資源
3.如果輸入內容不符合上面兩種情況將使用bing搜索此內容
// // KCMainViewController.m // UIWebView // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h"' #define kFILEPROTOCOL @"file://" @interface KCMainViewController (){ UIWebView *_webView; UIToolbar *_toolbar; UISearchBar *_searchBar; UIBarButtonItem *_barButtonBack; UIBarButtonItem *_barButtonForward; } @end @implementation KCMainViewController #pragma mark - 界面UI事件 - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark - 私有方法 #pragma mark 界面布局 -(void)layoutUI{ /*添加地址欄*/ _searchBar=[[UISearchBar alloc]initWithFrame:CGRectMake(0, 20, 320, 44)]; _searchBar.delegate=self; [self.view addSubview:_searchBar]; /*添加浏覽器控件*/ _webView=[[UIWebView alloc]initWithFrame:CGRectMake(0, 64, 320, 460)]; _webView.dataDetectorTypes=UIDataDetectorTypeAll;//數據檢測,例如內容中有郵件地址,點擊之後可以打開郵件軟件編寫郵件 _webView.delegate=self; [self.view addSubview:_webView]; /*添加下方工具欄*/ _toolbar=[[UIToolbar alloc]initWithFrame:CGRectMake(0, 524, 320, 44)]; UIButton *btnBack=[UIButton buttonWithType:UIButtonTypeCustom]; btnBack.bounds=CGRectMake(0, 0, 32, 32); [btnBack setImage:[UIImage imageNamed:@"back.png"] forState:UIControlStateNormal]; [btnBack setImage:[UIImage imageNamed:@"back_disable.png"] forState:UIControlStateDisabled]; [btnBack addTarget:self action:@selector(webViewBack) forControlEvents:UIControlEventTouchUpInside]; _barButtonBack=[[UIBarButtonItem alloc]initWithCustomView:btnBack]; _barButtonBack.enabled=NO; UIBarButtonItem *btnSpacing=[[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; UIButton *btnForward=[UIButton buttonWithType:UIButtonTypeCustom]; btnForward.bounds=CGRectMake(0, 0, 32, 32); [btnForward setImage:[UIImage imageNamed:@"forward.png"] forState:UIControlStateNormal]; [btnForward setImage:[UIImage imageNamed:@"forward_disable.png"] forState:UIControlStateDisabled]; [btnForward addTarget:self action:@selector(webViewForward) forControlEvents:UIControlEventTouchUpInside]; _barButtonForward=[[UIBarButtonItem alloc]initWithCustomView:btnForward]; _barButtonForward.enabled=NO; _toolbar.items=@[_barButtonBack,btnSpacing,_barButtonForward]; [self.view addSubview:_toolbar]; } #pragma mark 設置前進後退按鈕狀態 -(void)setBarButtonStatus{ if (_webView.canGoBack) { _barButtonBack.enabled=YES; }else{ _barButtonBack.enabled=NO; } if(_webView.canGoForward){ _barButtonForward.enabled=YES; }else{ _barButtonForward.enabled=NO; } } #pragma mark 後退 -(void)webViewBack{ [_webView goBack]; } #pragma mark 前進 -(void)webViewForward{ [_webView goForward]; } #pragma mark 浏覽器請求 -(void)request:(NSString *)urlStr{ //創建url NSURL *url; //如果file://開頭的字符串則加載bundle中的文件 if([urlStr hasPrefix:kFILEPROTOCOL]){ //取得文件名 NSRange range= [urlStr rangeOfString:kFILEPROTOCOL]; NSString *fileName=[urlStr substringFromIndex:range.length]; url=[[NSBundle mainBundle] URLForResource:fileName withExtension:nil]; }else if(urlStr.length>0){ //如果是http請求則直接打開網站 if ([urlStr hasPrefix:@"http"]) { url=[NSURL URLWithString:urlStr]; }else{//如果不符合任何協議則進行搜索 urlStr=[NSString stringWithFormat:@"http://m.bing.com/search?q=%@",urlStr]; } urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];//url編碼 url=[NSURL URLWithString:urlStr]; } //創建請求 NSURLRequest *request=[NSURLRequest requestWithURL:url]; //加載請求頁面 [_webView loadRequest:request]; } #pragma mark - WebView 代理方法 #pragma mark 開始加載 -(void)webViewDidStartLoad:(UIWebView *)webView{ //顯示網絡請求加載 [UIApplication sharedApplication].networkActivityIndicatorVisible=true; } #pragma mark 加載完畢 -(void)webViewDidFinishLoad:(UIWebView *)webView{ //隱藏網絡請求加載圖標 [UIApplication sharedApplication].networkActivityIndicatorVisible=false; //設置按鈕狀態 [self setBarButtonStatus]; } #pragma mark 加載失敗 -(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{ NSLog(@"error detail:%@",error.localizedDescription); UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"系統提示" message:@"網絡連接發生錯誤!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"確定", nil]; [alert show]; } #pragma mark - SearchBar 代理方法 #pragma mark 點擊搜索按鈕或回車 -(void)searchBarSearchButtonClicked:(UISearchBar *)searchBar{ [self request:_searchBar.text]; } @end
運行效果:
其實UIWebView整個使用相當簡單:創建URL->創建請求->加載請求,無論是加載本地文件還是Web內容都是這三個步驟。UIWebView內容加載事件同樣是通過代理通知外界,常用的代理方法如開始加載、加載完成、加載出錯等,這些方法通常可以幫助開發者更好的控制請求加載過程。
注意:UIWebView打開本地pdf、word文件依靠的並不是UIWebView自身解析,而是依靠MIME Type識別文件類型並調用對應應用打開。
UIWebView與頁面的交互主要體現在兩方面:使用ObjC方法進行頁面操作、在頁面中調用ObjC方法兩部分。和其他移動操作系統不同,iOS中所有的交互都集中於一個stringByEvaluatingJavaScriptFromString:方法中,以此來簡化開發過程。
1.首先在request方法中使用loadHTMLString:加載了html內容,當然你也可以將html放到bundle或沙盒中讀取並且加載。
2.然後在webViewDidFinishLoad:代理方法中通過stringByEvaluatingJavaScriptFromString:方法可以操作頁面中的元素,例如在下面的方法中讀取了頁面標題、修改了其中的內容。
// // KCMainViewController.m // UIWebView // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h"' @interface KCMainViewController (){ UIWebView *_webView; } @end @implementation KCMainViewController #pragma mark - 界面UI事件 - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; [self request]; } #pragma mark - 私有方法 #pragma mark 界面布局 -(void)layoutUI{ /*添加浏覽器控件*/ _webView=[[UIWebView alloc]initWithFrame:CGRectMake(0, 20, 320, 548)]; _webView.dataDetectorTypes=UIDataDetectorTypeAll;//數據檢測類型,例如內容中有郵件地址,點擊之後可以打開郵件軟件編寫郵件 _webView.delegate=self; [self.view addSubview:_webView]; } #pragma mark 浏覽器請求 -(void)request{ //加載html內容 NSString *htmlStr=@"\ \\ I am Kenshin Cui
\iOS 開發系列
\"; //加載請求頁面 [_webView loadHTMLString:htmlStr baseURL:nil]; } #pragma mark - WebView 代理方法 #pragma mark 開始加載 -(void)webViewDidStartLoad:(UIWebView *)webView{ //顯示網絡請求加載 [UIApplication sharedApplication].networkActivityIndicatorVisible=true; } #pragma mark 加載完畢 -(void)webViewDidFinishLoad:(UIWebView *)webView{ //隱藏網絡請求加載圖標 [UIApplication sharedApplication].networkActivityIndicatorVisible=false; //取得html內容 NSLog(@"%@",[_webView stringByEvaluatingJavaScriptFromString:@"document.title"]); //修改頁面內容 NSLog(@"%@",[_webView stringByEvaluatingJavaScriptFromString:@"document.getElementById('header').innerHTML='Kenshin Cui\\'s Blog'"]); } #pragma mark 加載失敗 -(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{ NSLog(@"error detail:%@",error.localizedDescription); UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"系統提示" message:@"網絡連接發生錯誤!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"確定", nil]; [alert show]; } @end
運行效果:
頁面中的js是無法直接調用ObjC方法的,但是可以變換一下思路:當需要進行一個js操作時讓頁面進行一個重定向,並且在重定向過程中傳入一系列參數。在UIWebView的代理方法中有一個webView: shouldStartLoadWithRequest:navigationType方法,這個方法會在頁面加載前執行,這樣可以在這裡攔截重定向,並且獲取定向URL中的參數,根據這些參數約定一個方法去執行。
當訪問百度搜索手機版時會發現,有時候點擊頁面中的某個元素可以調出iOS操作系統的UIActionSheet,下面不妨模擬一下這個過程。首先需要定義一個js方法,為了方便擴展,這個js保存在MyJs.js文件中存放到Bundle中,同時在頁面中加載這個文件內容。MyJs.js內容如下:
function showSheet(title,cancelButtonTitle,destructiveButtonTitle,otherButtonTitle) { var url='kcactionsheet://?'; var paramas=title+'&'+cancelButtonTitle+'&'+destructiveButtonTitle; if(otherButtonTitle){ paramas+='&'+otherButtonTitle; } window.location.href=url+ encodeURIComponent(paramas); } var blog=document.getElementById('blog'); blog.onclick=function(){ showSheet('系統提示','取消','確定',null); };
這個js的功能相當單一,調用showSheet方法則會進行一個重定向,調用過程中需要傳遞一系列參數,當然這些參數都是UIActionSheet中需要使用的,注意這裡約定所有調用UIActionSheet的方法參數都以”kcactionsheet”開頭。
然後在webView: shouldStartLoadWithRequest:navigationType方法中截獲以“kcactionsheet”協議開頭的請求,對於這類請求獲得對應參數調用UIActionSheet。看一下完整代碼:
// // KCMainViewController.m // UIWebView // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" @interface KCMainViewController (){ UIWebView *_webView; } @end @implementation KCMainViewController #pragma mark - 界面UI事件 - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; [self request]; } #pragma mark - 私有方法 #pragma mark 界面布局 -(void)layoutUI{ /*添加浏覽器控件*/ _webView=[[UIWebView alloc]initWithFrame:CGRectMake(0, 20, 320, 548)]; _webView.dataDetectorTypes=UIDataDetectorTypeAll;//數據檢測類型,例如內容中有郵件地址,點擊之後可以打開郵件軟件編寫郵件 _webView.delegate=self; [self.view addSubview:_webView]; } #pragma mark 顯示actionsheet -(void)showSheetWithTitle:(NSString *)title cancelButtonTitle:(NSString *)cancelButtonTitle destructiveButtonTitle:(NSString *)destructiveButtonTitle otherButtonTitles:(NSString *)otherButtonTitle{ UIActionSheet *actionSheet=[[UIActionSheet alloc]initWithTitle:title delegate:nil cancelButtonTitle:cancelButtonTitle destructiveButtonTitle:destructiveButtonTitle otherButtonTitles:otherButtonTitle, nil]; [actionSheet showInView:self.view]; } #pragma mark 浏覽器請求 -(void)request{ //加載html內容 NSString *htmlStr=@"\ \\ I am Kenshin Cui
\iOS 開發系列
\"; //加載請求頁面 [_webView loadHTMLString:htmlStr baseURL:nil]; } #pragma mark - WebView 代理方法 #pragma mark 頁面加載前(此方法返回false則頁面不再請求) -(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ if ([request.URL.scheme isEqual:@"kcactionsheet"]) { NSString *paramStr=request.URL.query; NSArray *params= [[paramStr stringByRemovingPercentEncoding] componentsSeparatedByString:@"&"]; id otherButton=nil; if (params.count>3) { otherButton=params[3]; } [self showSheetWithTitle:params[0] cancelButtonTitle:params[1] destructiveButtonTitle:params[2] otherButtonTitles:otherButton]; return false; } return true; } #pragma mark 開始加載 -(void)webViewDidStartLoad:(UIWebView *)webView{ //顯示網絡請求加載 [UIApplication sharedApplication].networkActivityIndicatorVisible=true; } #pragma mark 加載完畢 -(void)webViewDidFinishLoad:(UIWebView *)webView{ //隱藏網絡請求加載圖標 [UIApplication sharedApplication].networkActivityIndicatorVisible=false; //加載js文件 NSString *path=[[NSBundle mainBundle] pathForResource:@"MyJs.js" ofType:nil]; NSString *jsStr=[NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; //加載js文件到頁面 [_webView stringByEvaluatingJavaScriptFromString:jsStr]; } #pragma mark 加載失敗 -(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{ NSLog(@"error detail:%@",error.localizedDescription); UIAlertView *alert=[[UIAlertView alloc]initWithTitle:@"系統提示" message:@"網絡連接發生錯誤!" delegate:self cancelButtonTitle:nil otherButtonTitles:@"確定", nil]; [alert show]; } @end
運行效果:
前面無論是下載還是上傳都沒有考慮網絡狀態,事實上實際開發過程中這個問題是不得不思考的,試想目前誰會用3G或4G網絡下載一個超大的文件啊,因此實際開發過程中如果程序部署到了真機上必須根據不同的網絡狀態決定用戶的操作,例如下圖就是在使用QQ音樂播放在線音樂的提示:
網絡狀態檢查在早期都是通過蘋果官方的Reachability類進行檢查(需要自行下載),但是這個類本身存在一些問題,並且官方後來沒有再更新。後期大部分開發者都是通過第三方框架進行檢測,在這裡就不再使用官方提供的方法,直接使用AFNetworking框架檢測。不管使用官方提供的類還是第三方框架,用法都是類似的,通常是發送一個URL然後去檢測網絡狀態變化,網絡改變後則調用相應的網絡狀態改變方法。下面是一個網絡監測的簡單示例:
// // KCMainViewController.m // Network status // // Created by Kenshin Cui on 14-3-22. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" #import "AFNetworking.h" @interface KCMainViewController ()@end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; [self checkNetworkStatus]; } #pragma mark - 私有方法 #pragma mark 網絡狀態變化提示 -(void)alert:(NSString *)message{ UIAlertView *alertView=[[UIAlertView alloc]initWithTitle:@"System Info" message:message delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles: nil]; [alertView show]; } #pragma mark 網絡狀態監測 -(void)checkNetworkStatus{ //創建一個用於測試的url NSURL *url=[NSURL URLWithString:@"http://www.apple.com"]; AFHTTPRequestOperationManager *operationManager=[[AFHTTPRequestOperationManager alloc]initWithBaseURL:url]; //根據不同的網絡狀態改變去做相應處理 [operationManager.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { switch (status) { case AFNetworkReachabilityStatusReachableViaWWAN: [self alert:@"2G/3G/4G Connection."]; break; case AFNetworkReachabilityStatusReachableViaWiFi: [self alert:@"WiFi Connection."]; break; case AFNetworkReachabilityStatusNotReachable: [self alert:@"Network not found."]; break; default: [self alert:@"Unknown."]; break; } }]; //開始監控 [operationManager.reachabilityManager startMonitoring]; } @end
AFNetworking是網絡開發中常用的一個第三方框架,常用的網絡開發它都能幫助大家更好的實現,例如JSON數據請求、文件下載、文件上傳(並且支持斷點續傳)等,甚至到AFNetworking2.0之後還加入了對NSURLSession的支持。由於本文更多目的在於分析網絡操作原理,因此在此不再贅述,更多內容大家可以看官方文檔,常用的操作都有示例代碼。