我的前面两篇文章介绍了NSURLSession套件的使用和NSURLSession套件的主要类。今天我们使用NSURLSession来完成一个小的应用程序。在实战之前,我先补充一点,为什么苹果会主推NSURLSession技术,而放弃大家都熟悉的NSURLConnection技术,苹果这么做肯定是有原因的,下面列举了NSURLSession的优点:
1.后台上传和下载。当你的程序退出了也能进行网络操作,这对用户和APP来说都是个好消息,不用运行APP就可以下载和上传,这样更节约手机电量。
2.能够暂停和恢复网络操作。不需要使用NSOperation就可以实现暂停、继续、重启等操作。
3.可配置的容器。
4.可以子类化并且可以设置私有存储方式。可以修改数据的存储方式和存储位置。
5.改进了授权处理机制。
6.代理更强大。
7.通过文件系统上传和下载。
好了,进入整体开始我们的实战,开发一个小的APP叫《ByteClub》。这篇文章的实战我是参考国外的网站做的,原文《NSURLSession Tutorial》,地址:http://www.raywenderlich.com/51127/nsurlsession-tutorial。觉得它有点啰嗦,英文好的也可以看原文。
我没有去原文翻译它,参考它做完例子之后,我按照自己的思路写的本教程。
准备工作
1.如果你打算跟我一起动手做的话,您需要一个FQ工具,因为我需要使用dropbox(类似百度云盘)做http网络服务器,它在国内被墙掉了,我使用的是lantern.下载地址:http://pan.baidu.com/s/1hqhQqHI。下载完记得一定要安装和运行起来。
2.在dropbox网站注册成为开发者,然后创建一个APP。Dropbox开发者应用注册地址:https://www.dropbox.com/developers/apps。
3.然后下载dropbox for mac,我自己准备好了安装文件DropboxInstaller.dmg,下载地址:http://pan.baidu.com/s/1sjDvZNB。然后安装,安装好后在跟目录下创建byteclub的目录。如图:
4.请在byteclub目录中,随便创建或者复制进来几个文件,然后会同步到服务器上,然后我们的第一步开发工作,就是从服务器读取到这些文件的文件名。
5.然后再下载起始项目,代码把UI界面以及dropbox的http接口做好了封装,以便我们专注于NSURLSession部分的实战和学习。起始项目代码下载地址:http://pan.baidu.com/s/1mg8M2Vm。
6.如果您想查看最终效果,可以下载我实战完成后的代码。下载地址:http://pan.baidu.com/s/1sj48BAP
项目完成后的效果图:
您可以先稍微熟悉下初始项目。
开工干活
第一阶段:读取Dropbox的跟目录文件名,并显示。
1.打开Dropbox.m将您的apiKey和appSecret,appFolder设置进去,前两者认证需呀,后者是我们在准备阶段在Dropbox创建的目录,比如我的设置为:
static NSString *apiKey = @"rctz909lpd47vyq"; static NSString *appSecret = @“odz1qfezg4ij3pz"; NSString * const appFolder = @“byteclub";
然后你可以运行看看,按照引导信息,输入您在dropbox的账号信息,然后应该就可以认证通过了。
2.在NotesViewController.m文件中添加一个会话属性,用于保存我们的会话。
/** * 会话 */ @property (nonatomic, strong) NSURLSession *session;
3.在initWithStyle:前面添加另外一个初始化方法:
- (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; //下面内容为创建会话 if (self) { //会话配置,这里配置为短暂配置,还有默认配置和后台配置 NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; //配置请求头 [config setHTTPAdditionalHeaders:@{@"Authorization":[Dropbox apiAuthorizationHeader]}]; //初始化会话 _session = [NSURLSession sessionWithConfiguration:config]; } return self; }
4.找到notesOnDropbox:方法,然方法内输入如下代码:
//显示加载提示 [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; //获取你的Dropbox的根目录 NSURL *url = [Dropbox appRootURL]; //创建数据任务,这个方法主要用来请求HTTP的GET方法,并返回NSData对象,我们需要将数据再解析成我们需要的数据 NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (!error) { // 响应状态代码为200,代表请求数据成功,判断成功后我们再进行数据解析 NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response; if (httpResp.statusCode == 200) { NSError *jsonError; //解析NSData数据 NSDictionary *notesJSON = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError]; NSMutableArray *notesFound = [[NSMutableArray alloc] init]; if (!jsonError) { // 获取contents键值,文件路径保存在这里 NSArray *contentsOfRootDirectory = notesJSON[@"contents"]; for (NSDictionary *data in contentsOfRootDirectory) { if (![data[@"is_dir"] boolValue]) { DBFile *note = [[DBFile alloc] initWithJSONData:data]; [notesFound addObject:note]; } } //排序 [notesFound sortUsingComparator: ^NSComparisonResult(id obj1, id obj2) { return [obj1 compare:obj2]; }]; self.notes = notesFound; // NSURLSession的方法是在异步执行的,所以更新UI回到主线程 dispatch_async(dispatch_get_main_queue(), ^{ [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; [self.tableView reloadData]; }); } } } }]; //启动任务 [dataTask resume];
注释已经比较清晰,如果我们查看Dropbox的文档就会发现,私用这个URL返回的数据就是json数据,数据格式如下:
{ "hash": "6a29b68d106bda4473ffdaf2e94c4b61", "revision": 73052, "rev": "11d5c00e1cf6c", "thumb_exists": false, "bytes": 0, "modified": "Sat, 10 Aug 2013 21:56:50 +0000", "path": "/byteclub", "is_dir": true, "icon": "folder", "root": "dropbox", "contents": [{ "revision": 73054, "rev": "11d5e00e1cf6c", "thumb_exists": false, "bytes": 16, "modified": "Sat, 10 Aug 2013 23:21:03 +0000", "client_mtime": "Sat, 10 Aug 2013 23:21:02 +0000", "path": "/byteclub/test.txt", "is_dir": false, "icon": "page_white_text", "root": "dropbox", "mime_type": "text/plain", "size": "16 bytes" }], "size": "0 bytes" }
dropbox服务器的返回状态码有如下一些:
400 –代表参数有误.
401 – token错误或过期.
403 – 错误 OAuth 请求
404 – 请求的文件和目录不存在
405 – 请求方法错误,一般我们只使用get 和post
429 –程序请求次数过多
503 –请再次尝试
507 –使用Dropbox空间超过配额限制
5xx – 服务器错误
5.运行一下看看效果,我的效果是这样的:
第一阶段的实战完成,你可以先复习一下内容,然后再进行第二阶段的实战。
第二阶段的实战
通过APP输入内容,并通过文件的形式保存到Dropbox服务器并然后显示。
1.点击右上角的“+”按钮,跳转到添加Notes界面,你可以输入内容,但目前没什么作用,我们需要实现这些功能。打开NotesViewController.m文件,找到prepareForSegue:sender:跳转方法,在代码showNote.delegate = self后添加下面一行代码,将会话session传递到新的页面,这样我们就可以在新的页面使用同一个会话了:
//传递会话 showNote.session = _session;
2.找到新页面所属类的实现文件NoteDetailsViewController.m文件,找到 (IBAction)done:(id)sender方法,这是我们点击done按钮需要执行的方法,替换为如下代码:
- (IBAction)done:(id)sender { // must contain text in textview if (![_textView.text isEqualToString:@""]) { // check to see if we are adding a new note if (!self.note) { DBFile *newNote = [[DBFile alloc] init]; newNote.root = @"dropbox"; self.note = newNote; } _note.contents = _textView.text; _note.path = _filename.text; // - 上传文件到 DROPBOX - // // 获取需要上传文件的路径 NSURL *url = [Dropbox uploadURLForPath:_note.path]; // 创建请求,这里使用了put方法 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; [request setHTTPMethod:@"PUT"]; //数据 NSData *noteContents = [_note.contents dataUsingEncoding:NSUTF8StringEncoding]; // 上传任务,NSURLSessionUploadTask支持文件,NSData,数据流stream的类型数据上传 NSURLSessionUploadTask *uploadTask = [_session uploadTaskWithRequest:request fromData:noteContents completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { //根据HTTP返回的代号确定是否成功,200代表成功,成功后我调用代理方法 NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response; if (!error && httpResp.statusCode == 200) { [self.delegate noteDetailsViewControllerDoneWithDetails:self]; } else { // alert for error saving / updating note } }]; // 必须要的动作,启动任务 [uploadTask resume]; } else { UIAlertView *noTextAlert = [[UIAlertView alloc] initWithTitle:@"输入为空" message:@"总得输入点啥吧,亲" delegate:nil cancelButtonTitle:@"好" otherButtonTitles:nil]; [noTextAlert show]; } }
3.打开NoteDetailsViewController.m方法,找到方法retreiveNoteText:,替换为如下内容:
-(void)retreiveNoteText { // 根据Dropbox API设置要查看的note的请求路径 NSString *fileApi = @"https://api-content.dropbox.com/1/files/dropbox"; NSString *escapedPath = [_note.path stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]; NSString *urlStr = [NSString stringWithFormat: @"%@/%@", fileApi,escapedPath]; NSURL *url = [NSURL URLWithString: urlStr]; [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; // 执行下载数据任务 [[_session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error) { NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response; if (httpResp.statusCode == 200) { // 数据转字符串 NSString *text = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; dispatch_async(dispatch_get_main_queue(), ^{ [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; self.textView.text = text; }); } else { // 处理错误的响应 // } } else { // 处理意外错误 // } // 一定要创建任务后启动哦 }] resume]; }
4.运行看看,比如输入内容,点击done按钮,过一会儿你查看下你的byteclub目录,应该会创建一个新的文件。我的运行效果是这样的:
好了,我们的第二阶段任务就完成,还是建议您复习一下刚才的内容,然后我们开始第三阶段的实战。
第三阶段
使用NSURLSessionTask的代理方法发送图片到dropbox
1.请在byteclub目录下新建一个photos目录,然后拖一些你的图片到里面来,等待一会儿,图片应该就会上传完。
2.打开PhotosViewController.m,找到tableView:cellForRowAtIndexPath:方法,替换为下面内容:
3.相同文件,找到refreshPhotos方法,替换为下面内容:
//获取图片 - (void)refreshPhotos { //网络家提示开启 [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; //dropbox的API,这个接口用于搜索图片 NSString *photoDir = [NSString stringWithFormat:@"https://api.dropbox.com/1/search/dropbox/%@/photos?query=.jpg",appFolder]; NSURL *url = [NSURL URLWithString:photoDir]; //启动一个数据下载任务 [[_session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error) { NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response; //状态码为200请求成功 if (httpResp.statusCode == 200) { //返回的数据类型为json数组,解析 NSError *jsonError; NSArray *filesJSON = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError]; NSMutableArray *dbFiles = [[NSMutableArray alloc] init]; if (!jsonError) { for (NSDictionary *fileMetadata in filesJSON) { DBFile *file = [[DBFile alloc] initWithJSONData:fileMetadata]; [dbFiles addObject:file]; } [dbFiles sortUsingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj1 compare:obj2]; }]; //添加到数组中保存 _photoThumbnails = dbFiles; //更新主界面 dispatch_async(dispatch_get_main_queue(), ^{ [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; [self.tableView reloadData]; }); } } else { // 处理相应失败// } } else { // 这里处理失败 // } }] resume]; }
4.休息一下,运行看看,应该可以看到图片加载了,我的效果如图:
好了,现在我做上传图片的操作,并通过NSURLSessionDelegate和NSURLSessionTaskDelegate来帮助我们了解上传的状态和进度。
5.修改PhotosViewController.m使其遵守NSURLSessionTaskDelegate代理协议,代码如图:
@interface PhotosViewController ()<UITableViewDelegate, UITableViewDataSource, UIImagePickerControllerDelegate, UINavigationControllerDelegate, NSURLSessionTaskDelegate>
6.添加如下属性,用于保存上传任务:
@property (nonatomic, strong) NSURLSessionUploadTask *uploadTask;
7.修改uploadImage:方法,替代为如下内容:
- (void)uploadImage:(UIImage*)image { NSData *imageData = UIImageJPEGRepresentation(image, 0.6); // 配置一次只能对服务器一个连接 NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; config.HTTPMaximumConnectionsPerHost = 1; [config setHTTPAdditionalHeaders:@{@"Authorization": [Dropbox apiAuthorizationHeader]}]; // 初始化上传会话 NSURLSession *upLoadSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; // 上传任务的URL地址 NSURL *url = [Dropbox createPhotoUploadURL]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; [request setHTTPMethod:@"PUT"]; // 设置上传的图片数据 self.uploadTask = [upLoadSession uploadTaskWithRequest:request fromData:imageData]; // 上传进度 self.uploadView.hidden = NO; [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; // 启动任务 [_uploadTask resume]; }
8.最类最后面添加NSURLSessionTaskDelegate的两个方法实现:
#pragma mark - NSURLSessionTaskDelegate 方法 // /** *这个代理方法可以跟踪进度 * * @param session 会话 * @param task 任务 * @param bytesSent 正在接受到的数据大小 * @param totalBytesSent 总的接受的数据带大小 * @param totalBytesExpectedToSend 估算总共需要接受的数据大小 */ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend { //根据接收到数据大小和总的数据大小计算出进度条显示的进度值 dispatch_async(dispatch_get_main_queue(), ^{ [_progress setProgress: (double)totalBytesSent / (double)totalBytesExpectedToSend animated:YES]; }); } /** *当上传或下载数据成功时执行 * * @param session 会话 * @param task 任务 * @param error 错误 */ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { // 主线程更新进度,隐藏加载提示,隐藏上传进度 dispatch_async(dispatch_get_main_queue(), ^{ [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; _uploadView.hidden = YES; [_progress setProgress:0.5]; }); if (!error) { // 2 dispatch_async(dispatch_get_main_queue(), ^{ [self refreshPhotos]; }); } else { //处理错误 } }
9.找到cancelUpload:方法,当我们点击cancel取消时会调用此方法,这里我们需要取消上传任务:
// 停止上传 - (IBAction)cancelUpload:(id)sender { //取消上传任务 if (_uploadTask.state == NSURLSessionTaskStateRunning) { [_uploadTask cancel]; } }
10.哈哈,大功告成,运行一下看看效果吧
至此我通过三篇文章介绍了NSULRSession套件的原理,和它的常用类,然后通过一个实战例子告诉大家NSURLSession如何使用。谢谢大家!