IOS 開發不得不知道的網路知識

清風颺發表於2016-05-21

概覽

大部分應用程式都或多或少會牽扯到網路開發,例如說新浪微博、微信等,這些應用本身可能採用iOS開發,但是所有的資料支撐都是基於後臺網路伺服器的。如今,網路程式設計越來越普遍,孤立的應用通常是沒有生命力的。今天就會給大家介紹這部分內容:

  1. Web請求和響應
    1. 使用代理方法 
    2. 簡化請求方法 
    3. 圖片快取 
    4. 擴充套件--檔案分段下載 
    5. 擴充套件--檔案上傳 
  2. NSURLSession
    1. 資料請求 
    2. 檔案上傳 
    3. 檔案下載 
    4. 會話
  3. UIWebView
    1. 瀏覽器實現 
    2. UIWebView與頁面互動 
  4. 網路狀態
  5.  

Web請求和響應

使用代理方法

做過Web開發的朋友應該很清楚,Http是無連線的請求。每個請求request伺服器都有一個對應的響應response,無論是asp.net、jsp、php都是基於這種機制開發的。

requestAndResponse

在Web開發中主要的請求方法有如下幾種:

  • GET請求:get是獲取資料的意思,資料以明文在URL中傳遞,受限於URL長度,所以傳輸資料量比較小。 
  • POST請求:post是向伺服器提交資料的意思,提交的資料以實際內容形式存放到訊息頭中進行傳遞,無法在瀏覽器url中檢視到,大小沒有限制。 
  • HEAD請求:請求頭資訊,並不返回請求資料體,而只返回請求頭資訊,常用用於在檔案下載中取得檔案大小、型別等資訊。

在開發中往往資料儲存在伺服器端,而客戶端(iOS應用)往往通過向伺服器端傳送請求從伺服器端獲得資料。要模擬這個過程首先當然是建立伺服器端應用,應用的形式沒有限制,你可以採用任何Web技術進行開發。假設現在有一個檔案伺服器,使用者輸入檔名稱就可以下載檔案。伺服器端程式很簡單,只要訪問http://192.168.1.208/FileDownload.aspx?file=filename,就可以下載指定filename的檔案,由於伺服器端開發的內容不是今天的重點在此不再贅述。客戶端介面設計如下圖:

 DownloadLayout

程式的實現需要藉助幾個物件:

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 ()<NSURLConnectionDataDelegate>{
    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

執行效果:

NSURLConnectionEffect

需要注意:

  1. 根據響應資料大小不同可能會多次執行- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data方法。 
  2. URL中不能出現中文(例如上面使用GET傳引數時,file引數就可能是中文),需要對URL進行編碼,否則會出錯。

簡化請求方法

當然,對於上面檔案下載這種大資料響應的情況使用代理方法處理響應具有一定的優勢(可以獲得傳輸進度)。但是如果現響應資料不是檔案而是一段字串(注意web請求的資料可以是字串或者二進位制,上面檔案下載示例中響應資料是二進位制),那麼採用代理方法處理伺服器響應就未免有些太麻煩了。其實蘋果官方已經提供了下面兩種方法處理一般的請求:

+ (void)sendAsynchronousRequest:request: queue:queue:completionHandler:傳送一個非同步請求 

+ (NSData *)sendSynchronousRequest: returningResponse: error:傳送一個同步請求 

假設在開發一個類似於微博的應用,伺服器端返回的是JSON字串,我們可以使用上面的方法簡化整個請求響應的過程。這裡會使用在“iOS開發系列--UITableView全面解析”文章中自定義的UITableViewCell來顯示微博資料,不清楚的朋友可以看一下前面的內容。

請求過程中需要傳遞一個使用者名稱和密碼,如果全部正確則伺服器端返回此使用者可以看到的最新微博資料,響應的json格式大致如下:

WeiboJson

整個Json最外層是statuses節點,它是一個陣列型別,陣列中每個元素都是一條微博資料,每條微博資料中除了包含微博資訊還包含了發表使用者的資訊。

首先需要先定義使用者模型KCUser

//
//  KCUser.h
//  UrlConnection
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <Foundation/Foundation.h>

@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 <Foundation/Foundation.h>
#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 <UIKit/UIKit.h>
@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 ()<UITableViewDataSource,UITableViewDelegate>{
    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

執行效果:

WeboUI 

可以看到使用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;
}

執行效果:

WeboEffect

在上面的方法中直接呼叫了SDWebImage的分類快取方法設定圖片,這個方法可以分配另外一個執行緒去載入圖片(同時對於頭像還指定了預設圖片,網速較慢時不至於顯示空白),圖片載入後存放在沙箱的快取資料夾,如下圖:

WeiboImageCache

滾動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 ()<NSURLConnectionDataDelegate>{
    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

執行效果:

DownloadWithBlock

下載檔案的生成過程:

FileGenerateProgress分段下載的過程實現並不複雜,主要是需要配合後臺進行響應進行操作。針對不同的開發技術,伺服器端處理方式稍有差別,但是基本原理是一樣的,那就是讀取Range資訊,按需提供相應資料。

擴充套件--檔案上傳

在做WEB應用程式開發時,如果要上傳一個檔案往往會給form設定一個enctype=”multipart/form-data”的屬性,不設定這個值在後臺無法正常接收檔案。在WEB開發過程中,form的這個屬性其實本質就是指定請求頭中Content-Type型別,當然使用GET方法提交就不用說了,必須使用URL編碼。但是如果使用POST方法傳遞資料其實也是類似的,同樣需要進行編碼,具體編碼方式其實就是通過enctype屬性進行設定的。常用的屬性值有:

  • application/x-www-form-urlencoded:預設值,傳送前對所有傳送資料進行url編碼,支援瀏覽器訪問,通常文字內容提交常用這種方式。 
  • multipart/form-data:多部分表單資料,支援瀏覽器訪問,不進行任何編碼,通常用於檔案傳輸(此時傳遞的是二進位制資料) 。 
  • text/plain:普通文字資料型別,支援瀏覽器訪問,傳送前其中的空格替換為“+”,但是不對特殊字元編碼。 
  • application/json:json資料型別,瀏覽器訪問不支援 。 
  • text/xml:xml資料型別,瀏覽器訪問不支援。

要實現檔案上傳,必須採用POST上傳,同時請求型別必須是multipart/form-data。在Web開發中,開發人員不必過多的考慮mutiparty/form-data更多的細節,一般使用file控制元件即可完成檔案上傳。但是在iOS中如果要實現檔案上傳,就沒有那麼簡單了,我們必須瞭解這種資料型別的請求是如何工作的。 

下面是在瀏覽器中上傳一個檔案時,傳送的請求頭: 

Web_FileUpload_Header

這是傳送的請求體內容: 

Web_FileUpload_Body

在請求頭中,最重要的就是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 ()<NSURLConnectionDataDelegate>{
    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

NSURLSession

NSURLConnection是2003年伴隨著Safari一起發行的網路開發API,距今已經有十一年。當然,在這十一年間它表現的相當優秀,有大量的應用基礎,這也是為什麼前面花了那麼長時間對它進行詳細介紹的原因。但是這些年伴隨著iPhone、iPad的發展,對於NSURLConnection設計理念也提出了新的挑戰。在2013年WWDC上蘋果揭開了NSURLSession的面紗,將它作為NSURLConnection的繼任者。相比較NSURLConnection,NSURLSession提供了配置會話快取、協議、cookie和證照能力,這使得網路架構和應用程式可以獨立工作、互不干擾。另外,NSURLSession另一個重要的部分是會話任務,它負責載入資料,在客戶端和伺服器端進行檔案的上傳下載。

NSURLSession

通過前面的介紹大家可以看到,NSURLConnection完成的三個主要任務:獲取資料(通常是JSON、XML等)、檔案上傳、檔案下載。其實在NSURLSession時代,他們分別由三個任務來完成:NSURLSessionData、NSURLSessionUploadTask、NSURLSessionDownloadTask,這三個類都是NSURLSessionTask這個抽象類的子類,相比直接使用NSURLConnection,NSURLSessionTask支援任務的暫停、取消和恢復,並且預設任務執行在其他非主執行緒中,具體關係圖如下:

 NSURLSession_Class

資料請求

前面通過請求一個微博資料進行資料請求演示,現在通過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支援程式三種會話:

  1. defaultSessionConfiguration:程式內會話(預設會話),用硬碟來快取資料。 
  2. ephemeralSessionConfiguration:臨時的程式內會話(記憶體),不會將cookie、快取儲存到本地,只會放到記憶體中,當應用程式退出後資料也會消失。 
  3. 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 ()<NSURLSessionDownloadDelegate>{
    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_FileDownLoad

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 ()<NSURLSessionDownloadDelegate>{
    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更新。具體原理如下圖所示:transfer

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

UIWebView

網路開發中還有一個常用的UI控制元件UIWebView,它是iOS中內建的瀏覽器控制元件,功能十分強大。如一些社交軟體往往在應用程式內不需要開啟其他瀏覽器就能看一些新聞之類的頁面,就是通過這個控制元件實現的。需要注意的是UIWebView不僅能載入網路資源還可以載入本地資源,目前支援的常用的文件格式如:html、pdf、docx、txt等。

瀏覽器實現

下面將通過一個UIWebView開發一個簡單的瀏覽器,介面佈局大致如下:

WebBrowserLayout

在這個瀏覽器中將實現這樣幾個功能:

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 ()<UISearchBarDelegate,UIWebViewDelegate>{
    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

執行效果:

WebViewEffect  

其實UIWebView整個使用相當簡單:建立URL->建立請求->載入請求,無論是載入本地檔案還是Web內容都是這三個步驟。UIWebView內容載入事件同樣是通過代理通知外界,常用的代理方法如開始載入、載入完成、載入出錯等,這些方法通常可以幫助開發者更好的控制請求載入過程。

注意:UIWebView開啟本地pdf、word檔案依靠的並不是UIWebView自身解析,而是依靠MIME Type識別檔案型別並呼叫對應應用開啟。

UIWebView與頁面互動

UIWebView與頁面的互動主要體現在兩方面:使用ObjC方法進行頁面操作、在頁面中呼叫ObjC方法兩部分。和其他移動作業系統不同,iOS中所有的互動都集中於一個stringByEvaluatingJavaScriptFromString:方法中,以此來簡化開發過程。

在iOS中操作頁面

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 ()<UISearchBarDelegate,UIWebViewDelegate>{
    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=@"<html>\
            <head><title>Kenshin Cui's Blog</title></head>\
            <body style=\"color:#0092FF;\">\
                <h1 id=\"header\">I am Kenshin Cui</h1>\
                <p>iOS 開發系列</p>\
            </body></html>";
    
    //載入請求頁面
    [_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

執行效果:

UIWebView_JavascriptEffect  

頁面中呼叫ObjC方法

頁面中的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 ()<UISearchBarDelegate,UIWebViewDelegate>{
    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=@"<html>\
            <head><title>Kenshin Cui's Blog</title></head>\
            <body style=\"color:#0092FF;\">\
                <h1 id=\"header\">I am Kenshin Cui</h1>\
                <p id=\"blog\">iOS 開發系列</p>\
            </body></html>";
    
    //載入請求頁面
    [_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

執行效果:

WebView_JavascriptEffect2

網路狀態

前面無論是下載還是上傳都沒有考慮網路狀態,事實上實際開發過程中這個問題是不得不思考的,試想目前誰會用3G或4G網路下載一個超大的檔案啊,因此實際開發過程中如果程式部署到了真機上必須根據不同的網路狀態決定使用者的操作,例如下圖就是在使用QQ音樂播放線上音樂的提示:

QQMusic

網路狀態檢查在早期都是通過蘋果官方的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 ()<NSURLConnectionDataDelegate>

@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的支援。由於本文更多目的在於分析網路操作原理,因此在此不再贅述,更多內容大家可以看官方文件,常用的操作都有示例程式碼。

轉載:

http://www.cnblogs.com/kenshincui/p/4042190.html

相關文章