使用 NSURLSession 开发一个支持后台下载和断点续传的下载工具

NSURLSession 是 iOS 系统提供给我们的原生网络操作库,它提供了网络操作相关的一系列特性支持,比如缓存控制,Cookie管理,HTTP 认证处理等等,是一套整体的网络操作处理解决方案。

关于 NSURLSession 的基本特性,我们之前的一篇文章 NSURLSession 网络库 - 原生系统送给我们的礼物 有过详细的介绍,如果大家之前没有使用过这个库,可以先参考一下这篇内容。

这次我们不介绍任何 NSURLSession 的基础概念,我们将以一个实际的下载工具 APP 开发来实际的感受 NSURLSession 的大部分特性。那么闲言碎语不要讲,咱们就开始吧~

创建项目

首先,我们使用 Xcode 将我们的下载工具 APP 的项目建立好,点击 Xcode 菜单中 File -> New -> Project… 然后在弹出的窗口中选择项目的模板为 Tabbed Application:

使用 NSURLSession 开发一个支持后台下载和断点续传的下载工具_第1张图片

接下来,输入 Product Name 为 downloader,然后选择项目语言为 Swift:

使用 NSURLSession 开发一个支持后台下载和断点续传的下载工具_第2张图片

然后点击 Next 按钮,选择一个合适的位置存放你的项目文件。

项目创建完成后,运行 APP,就会看到为我们生成的默认界面了:

建立模型

开发一个下载管理工具,首先我们要建立下载任务的模型。

DownloadTask

我们建立一个 DownloadTask 结构用于表示下载任务:

struct DownloadTask {
    
    var url: NSURL
    var localURL:NSURL?
    var taskIdentifier: Int
    var finished:Bool = false
    
    init(url:NSURL, taskIdentifier: Int) {
        
        self.url = url
        self.taskIdentifier = taskIdentifier
        
    }
    
}

它包含4个属性,url 表示当前下载任务的 url 地址。localURL 表示下载成功后保存到本地文件的位置,taskIdentifier 作为这个下载任务的唯一标识。 finished 表示当前下载任务是否完成。

TaskManager

模型建立完成后,我们再创建一个 TaskManager 类,用于管理下载任务:

class TaskManager: NSObject, NSURLSessionDownloadDelegate {

    private var session:NSURLSession?
    
    var taskList:[DownloadTask] = [DownloadTask]()
    
    static var sharedInstance:TaskManager = TaskManager()
    
    override init() {
        
        super.init()
        
        let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("downloadSession")
        self.session = NSURLSession(configuration: config, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
        self.taskList = [DownloadTask]()
        self.loadTaskList()

    }
	
	//...
}

我们来看一下 TaskManager 的定义,它继承自 NSObject,并且实现了 NSURLSessionDownloadDelegate协议。这个协议用于检测下载进度,稍后我们会介绍到。

  • 它还定义了一个属性 private var session:NSURLSession? 作为我们所有下载任务所需的 NSURLSession 实例。
  • var taskList:[DownloadTask] = [DownloadTask]() 这个属性用于保存和跟踪我们所有的下载任务记录。用我们开始定义的 DownloadTask 结构表示。
  • static var sharedInstance:TaskManager = TaskManager() 表示 TaskManager 类的单例实例。

在 init() 方法中,我们使用 backgroundSessionConfigurationWithIdentifier 初始化了一个 background 类型的 NSURLSession 配置信息。这个配置可以让下载会话在应用切换到后台的情况下还能继续进行。

随后我们用这个配置信息对象初始化我们的 NSURLSession,指定它的代理类,以及代理类所在的线程队列。随后通过:

self.taskList = [DownloadTask]()
self.loadTaskList()

这两行代码初始化应用的下载任务列表,self.loadTaskList() 也是 TaskManager 类中的方法,我们稍后会详细介绍。

TaskManager 任务管理类建立好了,我们还需要它提供两个任务列表,一个是正在下载的任务,一个是已经下载完成的任务,这样我们的前端 UI 就可以将相应的任务显示给用户了:

func unFinishedTask() -> [DownloadTask] {
    
    return taskList.filter{ task in
        
        return task.finished == false
        
    }
    
}

func finishedTask() -> [DownloadTask] {
    
    return taskList.filter { task in
        
        return task.finished
        
    }
    
}

unFinishedTask() 和 finishedTask() 方法分别对象了正在下载的任务列表和已经下载完成的任务列表。并且他们的实现也非常简单,我们用到了 Swift 中 Array 提供的 filter 方法,来过滤列表中的数据。 filter 方法会遍历整个集合,并为每一个集合项提供一个闭包,我们只需要在闭包中返回一个 bool 值,来表示当前这个集合项是否符合条件。

两个列表方法提供好后,我们还需要另外一对工具方法。我们的下载任务是需要进行持久化保存的,这样我们每次重新打开 APP 就不会丢失以前建立的任务了,这也是一个下载工具必备的功能。我们这里将任务信息转换成 JSON 数据存储起来,以便下次打开 APP 的时候能够恢复任务数据:

func saveTaskList() {
    
    let jsonArray = NSMutableArray()

    for task in taskList {
        
        let jsonItem = NSMutableDictionary()
        jsonItem["url"] = task.url.absoluteString
        jsonItem["taskIdentifier"] = NSNumber(long: task.taskIdentifier)
        jsonItem["finished"] = NSNumber(bool: task.finished)
        
        jsonArray.addObject(jsonItem)
        
    }

    do {
        
        let jsonData = try NSJSONSerialization.dataWithJSONObject(jsonArray, options: NSJSONWritingOptions.PrettyPrinted)
        NSUserDefaults.standardUserDefaults().setObject(jsonData, forKey: "taskList")
        NSUserDefaults.standardUserDefaults().synchronize()
        
    }catch {
        
    }
    
}

func loadTaskList() {
    
    if let jsonData:NSData = NSUserDefaults.standardUserDefaults().objectForKey("taskList") as? NSData {
        
        do {
            
            guard let jsonArray:NSArray = try NSJSONSerialization.JSONObjectWithData(jsonData, options: NSJSONReadingOptions.AllowFragments) as? NSArray else { return }
            
            for jsonItem in jsonArray {
                
                if let item:NSDictionary = jsonItem as? NSDictionary {
                    
                    guard let urlstring = item["url"] as? String else { return }
                    guard let taskIdentifier = item["taskIdentifier"]?.longValue else { return }
                    guard let finished = item["finished"]?.boolValue else { return }
                    
                    let downloadTask = DownloadTask(url: NSURL(string: urlstring)!, taskIdentifier: taskIdentifier)
                    self.taskList.append(downloadTask)
                    
                }
                
            }
            
        } catch {
            
        }
        
    }
    
}

saveTaskList() 和 loadTaskList() 方法分别用于保存和加载任务数据,这两个方法的实现都是使用 JSON 数据和 NSUserDefaults 的相关操作,我们就不详细展开了。

我们接下来还需要一个方法来建立新的下载任务:

func newTask(url: String) {

    
    if let url = NSURL(string: url) {
        
        let downloadTask = self.session?.downloadTaskWithURL(url)
        downloadTask?.resume()
        
        let task = DownloadTask(url: url, taskIdentifier: downloadTask!.taskIdentifier)
        self.taskList.append(task)
        self.saveTaskList()
        		
    }
        
}

newTask 方法接受一个 url 参数,代表我们这个新任务的下载地址,使用 self.session?.downloadTaskWithURL(url) 来建立一个新的 NSURLSession 的下载任务。接着,我们调用 downloadTask?.resume() 启动这个任务。

下载任务建立好了,我们还需要将它的信息保存起来,就需要实例化一个我们之前定义的 DownloadTask 类,然后将它加入到我们的存储列表 self.taskList 中。最后调用 self.saveTaskList() 将任务数据保存到持久化层中。

到现在,我们已经建立好了一套基础机制,可以新建并开启下载任务了。当然,我们还需要对下载任务进行一些处理,比如下载完成时候要将任务状态重新标记,以及实时显示任务的下载进度,这就需要用到我们最开始实现的 NSURLSessionDownloadDelegate 协议的方法,现在我们来完善它:

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
    
    var fileName = ""
    
    for var i = 0;i < self.taskList.count;i++ {
        
        if self.taskList[i].taskIdentifier == downloadTask.taskIdentifier {

            self.taskList[i].finished = true
            fileName = self.taskList[i].url.lastPathComponent!
            
        }
        
    }
    
    if let documentURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first {
        
        let destURL = documentURL.URLByAppendingPathComponent(fileName)
        do {

            try NSFileManager.defaultManager().moveItemAtURL(location, toURL: destURL)
            
        } catch {
            
        }

        
    }
    
    self.saveTaskList()
    
    NSNotificationCenter.defaultCenter().postNotificationName(DownloadTaskNotification.Finish.rawValue, object: downloadTask.taskIdentifier)
    
}

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {

    
}

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
    
    let progressInfo = ["taskIdentifier": downloadTask.taskIdentifier,
                        "totalBytesWritten": NSNumber(longLong: totalBytesWritten),
                        "totalBytesExpectedToWrite": NSNumber(longLong: totalBytesExpectedToWrite)]

    NSNotificationCenter.defaultCenter().postNotificationName(DownloadTaskNotification.Progress.rawValue, object: progressInfo)

}

NSURLSessionDownloadDelegate 协议中定义了三个方法:

URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL)
URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64)
URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)

didFinishDownloadingToURL 在下载任务完成的时候被调用, didResumeAtOffset 在下载任务恢复的时候被调用, didWriteData 在下载任务进度发生变化的时候被调用。

我们先来看一下 didFinishDownloadingToURL 的实现,首先我们遍历整个 self.taskList 列表,通过 taskIdentifier 找到我们完成的这个下载任务对应的 DownloadTask 对象,然后将它的 finished 属性设置为 true,随后通过 fileName 将这个下载任务的文件名暂存下来。

这个协议方法中的 location 参数表示下载成功后,文件的暂存位置。使用 NSURLSession 下载的文件,都会先存放到一个临时目录中,我们需要将这个文件从临时目录中移动到我们 APP 的持久目录中,也就是我们接下来做的事情:

if let documentURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first {
    
    let destURL = documentURL.URLByAppendingPathComponent(fileName)
    do {

        try NSFileManager.defaultManager().moveItemAtURL(location, toURL: destURL)
        
    } catch {
        
    }

    
}

移动完成后,我们调用 self.saveTaskList() 方法保存当前的任务信息。最后我们会发送一个通知消息,让前端 UI 相应的进行刷新操作:

NSNotificationCenter.defaultCenter().postNotificationName(DownloadTaskNotification.Finish.rawValue, object: downloadTask.taskIdentifier)

这样我们的 didFinishDownloadingToURL 方法就实现完成了。注意通知的名称 DownloadTaskNotification.Finish.rawValue 是一个枚举值,使用枚举来定义通知的名称的好处是复用起来比较方便,我们来看看这个枚举的定义:

enum DownloadTaskNotification: String {
    
    case Progress = "downloadNotificationProgress"
    case Finish = "downloadNotificationFinish"
    
}

定义了两个枚举项,Finish 和 Progress,分别用于表示下载完成和下载进度变化。

我们继续分析 didWriteData 方法的定义,这个方法做的事情就是将当前的任务进度相关信息分装成一个 Dictionary, 然后通过通知的方式发送给前端进行处理:

let progressInfo = ["taskIdentifier": downloadTask.taskIdentifier,
                    "totalBytesWritten": NSNumber(longLong: totalBytesWritten),
                    "totalBytesExpectedToWrite": NSNumber(longLong: totalBytesExpectedToWrite)]

NSNotificationCenter.defaultCenter().postNotificationName(DownloadTaskNotification.Progress.rawValue, object: progressInfo)

到此我们这个下载工具基础模型就搭建好了,接下来我们就可以开始进行前端 UI 的创建了,即将看到效果咯。

交互与 UI

我们的这个下载 APP 是 Tab 结构的,这在最开始建立项目的时候我们已经看到,我们可以打开项目的 storyboard 文件,看到当前的 UI 结构:

使用 NSURLSession 开发一个支持后台下载和断点续传的下载工具_第3张图片

两个 Tab,每一个 Tab 直接关联到一个 UIViewController。我们需要对现有结构进行一些修改,我们在 UIViewController 和 Tab 之间再加入一层 UINavigationController。

拖放两个 UINavigationController 到 storyboard 中,切断 First View 和 Second View 两个控制器与 Tab 控制器的关联。然后将这两个控制器与两个 UINavigationController 的 rootViewController 关联起来,在将 UINavigationController 与 Tab 控制器关联起来,修改后的结构如下:

使用 NSURLSession 开发一个支持后台下载和断点续传的下载工具_第4张图片

我们进入到 FirstViewController 代码中(也就是第一个 Tab 对应的控制器),为它添加两个属性:

class FirstViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
	
	private var mainTableView: UITableView?    
	private var taskList: [DownloadTask]?
	
	//...
}

FirstViewController 实现了 UITableViewDelegate 和 UITableViewDataSource 协议。 mainTableView 用于显示我们的任务列表,taskList 表示我们当前显示的任务列表。

接着,我们实现 ViewDidLoad 方法:

override func viewDidLoad() {
	
    super.viewDidLoad()
    
    self.title = "正在下载"
    self.navigationController?.navigationBar.translucent = false
    self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Add, target: self, action: Selector("addTask"))
    
    self.mainTableView = UITableView(frame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height))
    self.mainTableView?.delegate = self
    self.mainTableView?.dataSource = self
    self.view.addSubview(self.mainTableView!)
    
    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("reloadData"), name: DownloadTaskNotification.Finish.rawValue, object: nil)
    
}

这里设置的这个控制器的 title 属性,以及 navigationBar 的透明度。然后创建了一个导航条按钮,用来添加任务。接着创建了 UITableView 对象,用来显示任务列表。最后,将我们的这个类的 reloadData 方法注册给 DownloadTaskNotification.Finish 通知。每当有任务下载完成的时候,我们都会重新加载列表内容。

接下来,我们创建重新加载数据的方法:

override func viewDidAppear(animated: Bool) {
    
    self.reloadData()
    
}

func reloadData() {
    
    taskList = TaskManager.sharedInstance.unFinishedTask()
    self.mainTableView?.reloadData()
    
}

每次在 viewDidAppear 中,我们会调用 self.reloadData() 方法重新加载数据,reloadData() 方法中使用我们之前定义的 TaskManager.sharedInstance.unFinishedTask() 方法获取正在下载的任务列表。最后调用 self.mainTableView?.reloadData() 方法刷新 UITableView 的显示。

我们接下来在实现 UITableView 的一系列代理方法:

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    
    return 1
    
}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    
    return 70.0
    
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
    return self.taskList == nil ? 0 : self.taskList!.count
    
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    
    var cell = tableView.dequeueReusableCellWithIdentifier("Cell") as? DownloadTaskCell
    
    if cell == nil {
        
        cell = DownloadTaskCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
        
    }
    
    cell?.updateData((self.taskList?[indexPath.row])!)
    return cell!
    
}

都是比较常用的逻辑,不需要过多展开了。唯一需要注意的一点是,这里用到了一个我们自定义的类 DownloadTaskCell,我们接下来就创建这个类:

class DownloadTaskCell: UITableViewCell {
	
    var labelName: UILabel = UILabel()
    var labelSize: UILabel = UILabel()
    var labelProgress: UILabel = UILabel()    
    var downloadTask: DownloadTask?
	
	//...
	
}

这个类继承自 UITableViewCell,它定义了 3 个 UILabel,分别用于显示下载任务的名称,已完成情况和下载进度。 还定义了一个 downloadTask 属性,表示当前单元格所表示的是哪一个下载任务。

再来看看它的初始化方法:

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    
    self.addSubview(labelName)
    self.addSubview(labelSize)
    self.addSubview(labelProgress)
    
    self.labelName.font = UIFont.systemFontOfSize(14)
    self.labelSize.font = UIFont.systemFontOfSize(14)
    self.labelProgress.font = UIFont.systemFontOfSize(14)
    
    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("updateProgress:"), name: DownloadTaskNotification.Progress.rawValue, object: nil)
    
}

将 UILabel 添加到视图中并设置了它们的字体,最后将 updateProgress 方法注册为 DownloadTaskNotification.Progress 通知,这样我们在下载任务进度变化的时候,就可以及时更新 UI 显示了。

接下来定义 updateProgress 方法:

 func updateProgress(notification: NSNotification) {
        
    guard let info = notification.object as? NSDictionary else { return }
    
    if let taskIdentifier = info["taskIdentifier"] as? NSNumber {
		
        if taskIdentifier.integerValue == self.downloadTask?.taskIdentifier {
			
            guard let written = info["totalBytesWritten"] as? NSNumber else { return }
            guard let total = info["totalBytesExpectedToWrite"] as? NSNumber else { return }
            
            let formattedWrittenSize = NSByteCountFormatter.stringFromByteCount(written.longLongValue, countStyle: NSByteCountFormatterCountStyle.File)            
            let formattedTotalSize = NSByteCountFormatter.stringFromByteCount(total.longLongValue, countStyle: NSByteCountFormatterCountStyle.File)
            
            self.labelSize.text = "\(formattedWrittenSize) / \(formattedTotalSize)"
            let percentage = Int((written.doubleValue / total.doubleValue) * 100.0)
            self.labelProgress.text = "\(percentage)%"
            
        }
        
    }
    
}

首先我们将之前封装传入进来的任务进度信息解析出来,还记不记得我们在 TaskManager 中定义的这个结构:

let progressInfo = ["taskIdentifier": downloadTask.taskIdentifier,
                    "totalBytesWritten": NSNumber(longLong: totalBytesWritten),
                    "totalBytesExpectedToWrite": NSNumber(longLong: totalBytesExpectedToWrite)]

这里解析的正式这个数据。首先我们取出 info[“taskIdentifier”], 然后将它与我们当前单元格对应的 DownloadTask 的 taskIdentifier 进行对比:

if taskIdentifier.integerValue == self.downloadTask?.taskIdentifier

如果对比成功,是当前的任务进度发生变化,那么就需要对 UI 进行更新,接下来,我们就取出其余的任务进度信息,然后将他们设置到 labelSize 和 labelProgress 的文本中:

guard let written = info["totalBytesWritten"] as? NSNumber else { return }
guard let total = info["totalBytesExpectedToWrite"] as? NSNumber else { return }

let formattedWrittenSize = NSByteCountFormatter.stringFromByteCount(written.longLongValue, countStyle: NSByteCountFormatterCountStyle.File)            
let formattedTotalSize = NSByteCountFormatter.stringFromByteCount(total.longLongValue, countStyle: NSByteCountFormatterCountStyle.File)

self.labelSize.text = "\(formattedWrittenSize) / \(formattedTotalSize)"
let percentage = Int((written.doubleValue / total.doubleValue) * 100.0)
self.labelProgress.text = "\(percentage)%"

我们继续定义一个 updateData 方法,用它来绑定 DownloadTask 信息:

func updateData(task : DownloadTask) {
    
    self.downloadTask = task
    labelName.text = self.downloadTask?.url.lastPathComponent
 
}

然后覆盖 layoutSubviews 方法,用来布局 UI 界面:

 override func layoutSubviews() {
        
	super.layoutSubviews()
	self.labelName.frame = CGRectMake(20, 10, self.contentView.frame.size.width - 50, 20)
	self.labelSize.frame = CGRectMake(20, 40, self.contentView.frame.size.width - 50, 20)
	self.labelProgress.frame = CGRectMake(self.contentView.frame.size.width - 45, 20, 40, 30)
	
}

这样, DownloadTaskCell 类的实现就完成了。

添加任务

对任务的显示,更新这些操作都处理完了,我们还需要一个 UI 来添加新的下载任务,创建一个新的类 NewTaskViewController :

class NewTaskViewController: UIViewController {

    var textView:UITextView?
    
    override func viewDidLoad() {
        
        super.viewDidLoad()
        self.title = "添加任务"
        self.view.backgroundColor = UIColor.whiteColor()
        
        self.navigationController?.navigationBar.translucent = false
        
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "下载", style: UIBarButtonItemStyle.Plain, target: self, action: Selector("add"))
        self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: UIBarButtonItemStyle.Plain, target: self, action: Selector("close"))
        
        self.textView = UITextView(frame: CGRectMake(10, 10, self.view.frame.size.width - 20, 250))
        self.textView?.layer.borderWidth = 1
        self.textView?.layer.borderColor = UIColor.grayColor().CGColor
        self.textView?.layer.cornerRadius = 5
        self.textView?.font = UIFont.systemFontOfSize(14)
        self.view.addSubview(self.textView!)

    }
    
    override func viewDidAppear(animated: Bool) {
        
        self.textView?.becomeFirstResponder()
        
    }
    
    func close() {
        
        self.dismissViewControllerAnimated(true, completion: nil)
        
    }
    
    func add() {
		        
        TaskManager.sharedInstance.newTask(self.textView!.text)
        self.dismissViewControllerAnimated(true, completion: nil)
        
        
    }
  
}

这个类有一个 textView 控件,用于输入下载链接,导航条的两个按钮分别对应 close() 和 add() 方法。 close 方法直接关闭这个界面。 add 方法会调用  TaskManager.sharedInstance.newTask(self.textView!.text)方法开启并添加下载任务,然后关闭添加界面。

我们还需要给这个添加界面建立一个入口,回到 FirstViewController 类中,我们添加一个方法:

 func addTask() {
        
    let viewController = NewTaskViewController()
    let navController = UINavigationController(rootViewController: viewController)
    self.presentViewController(navController, animated: true, completion: nil)
    
}

好了,下载工具的基础结构都搭建好了,现在你可以试着运行一下程序了。

点击添加任务按钮后,我们可以输入一个下载地址,比如 https://nodejs.org/dist/v4.2.3/node-v4.2.3.pkg:

使用 NSURLSession 开发一个支持后台下载和断点续传的下载工具_第5张图片

点击好确定后,下载任务即可开启:

这时候,你可以试着将 APP 切换到后台,过几分钟后再重新进入 APP, 你会发现下载任务的进度增长了一大块,或是下载任务已经完成了。这时因为在你切换到后台这段时间,NSURLSession 还在后台继续进行下载任务。

APP 运行一会儿之后,下载任务完成了,这时你会看到 “正在下载” 任务列表中的这个任务 “消失” 了,是因为我们注册了通知,当有下载任务完成的时候,我们就会刷新列表显示,刷新后只会把还没有下载完成的任务显示出来。所以我们刚刚那个完成的任务就看不到了。

显示已下载任务

所以我们还需要一个地方来显示我们已经下载完成的任务,我们打开 SecondViewController, 同样,给它添加一个 mainTableView 和 taskList 属性:

class SecondViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
	
    private var mainTableView:UITableView?    
    private var taskList: [DownloadTask]?
	
	//...
	
}

实现它的 viewDidLoad 方法:

 override func viewDidLoad() {
        
    super.viewDidLoad()
    self.title = "已完成"
    
    self.navigationController?.navigationBar.translucent = false
    
    self.mainTableView = UITableView(frame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height))
    self.mainTableView?.delegate = self
    self.mainTableView?.dataSource = self
    self.view.addSubview(self.mainTableView!)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("reloadData"), name: DownloadTaskNotification.Finish.rawValue, object: nil)
    
}

这里也是进行了控件初始化和注册通知,我们再来创建处理通知的 reloadData 方法:

override func viewDidAppear(animated: Bool) {
    
    self.reloadData()
    
}

func reloadData() {
    
    self.taskList = TaskManager.sharedInstance.finishedTask()
    self.mainTableView?.reloadData()
    
}

和 “正在下载” 列表不同的是,这次我们使用 TaskManager.sharedInstance.finishedTask() 方法来获取任务列表,我们只获取那些已经完成的下载任务,随后重新加载 UITableView 数据。

最后,我们再实现 UITableView 的代理方法:

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return 70.0
}

func numberOfSectionsInTableView(tableView: UITableView) -> Int {

    return 1
    
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    
    return self.taskList == nil ? 0 : self.taskList!.count
    
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    
    var cell = tableView.dequeueReusableCellWithIdentifier("FinishedCell") as? DownloadTaskCell
    
    if cell == nil {
        
        cell = DownloadTaskCell(style: UITableViewCellStyle.Default, reuseIdentifier: "FinishedCell")
        
    }
    
    cell?.updateData(self.taskList![indexPath.row])
    
    return cell!
    
}

这里面的实现方法没有太多变化。这样我们再重新运行应用,点击第二个 “已完成” Tab,就可以看到我们刚才下载完成的任务了。

结语

回顾一下。这次通过一个完整的 APP 开发向大家展示了 NSURLSession 在实际开发中的应用。更主要在下载操作方面的应用。我们首先创建了 background 类型的 NSURLSession,这种类型的 Session 建立的下载任务可以在应用切换到后台后继续运行,并且我们通过代理和通知机制,非常轻便的就实现了 UI 界面的及时更新。

当然,NSURLSession 的特性还有很多,比如我们这里还没有实现断点续传,以及如果在后台下载完成后的相应处理。大家也可以参考一下相关文档发挥一下自己的思考,同样,我稍后还会为大家继续完善这个例子,以及关于刚才说的断点续传这些功能的文章。

这里很多地方用到了 guard 关键字,这个是 Swift 2.0 版本新提供的特性,如果需要对这个进行了解,可以参考这里:guard 关键字

你可能感兴趣的:(使用,NSURLSession,开发一个)