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

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

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

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

创建项目

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

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

然后点击 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 结构:

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

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

我们进入到 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:

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

这时候,你可以试着将 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
关键字

时间: 2024-10-20 03:56:10

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

python 开发一个支持多用户在线的FTP

### 作者介绍:* author:lzl### 博客地址:* http://www.cnblogs.com/lianzhilei/p/5813986.html### 功能实现 作业:开发一个支持多用户在线的FTP程序 要求: 用户加密认证 允许同时多用户登录 每个用户有自己的家目录 ,且只能访问自己的家目录 对用户进行磁盘配额,每个用户的可用空间不同 允许用户在ftp server上随意切换目录 允许用户查看当前目录下文件 允许上传和下载文件,保证文件一致性 文件传输过程中显示进度条 附加功能

开发一个支持多用户在线的FTP程序

一,项目题目:开发一个支持多用户在线的FTP程序 二,项目要求: 1.用户加密认证 2.允许同时多用户登录 3.每个用户有自己的家目录 ,且只能访问自己的家目录 4.对用户进行磁盘配额,每个用户的可用空间不同 5.允许用户在ftp server上随意切换目录 6.允许用户查看当前目录下文件 7.允许上传和下载文件,保证文件一致性(md5) 8.文件传输过程中显示进度条 9.附加功能:支持文件的断点续传 三,注意事项: 基本要求. 完成1,2,3,5,6,7,8 实力选手. 完成 上条 及需求4

基于c++11新标准开发一个支持多线程高并发的网络库

背景 新的c++11标准出后,c++语法得到了很多的扩展,比起以往任何时候都要灵活和高效,提高了程序编码的效率,为软件开发人员节省了不少的时间. 之前我也写过基于ACE的网络服务器框架,但ACE毕竟有些臃肿,内部对象关系错综复杂,容易给人造成只见树木不见森林的错觉. 所以打算用c++11开发一个较为简洁,高效,支持高并发的网络库. 开源         花了两三周,终于把基础的结构开发完成,代码也开源在github上,网址是 https://github.com/lichuan/fly 欢迎各位

【iOS开发-网络】大文件的断点续传(断点下载)

要想实现断点续传,暂停的时候要取消connection连接, 开始下载的时候要给服务器发送头信息,告诉他要请求多长的数据,从哪里开始 //暂停 if(sender.selected) { [self.conn cancel];//取消连接 self.conn = nil; //开始 } else { //创建url NSURL *url = [NSURL URLWithString:@"http://localhost:8080/TFServer/resources/videos/minion_

如何基于Restful ABAP Programming模型开发并部署一个支持增删改查的Fiori应用

Jerry之前的文章30分钟用Restful ABAP Programming模型开发一个支持增删改查的Fiori应用 发布之后,有朋友问我,"没错, 我是在你的文章里看到了Fiori应用的界面,可是这个Fiori应用的源代码我在SAP云平台上什么地方能看到呢?这个Fiori应用部署之后的状态,我在哪里能够查看呢?" 这位朋友的发问非常有力,实际上,Jerry前一篇文章,离Fiori应用的开发和部署这个目标还有一半的距离.我们回忆下当时是如何基于开发完成的Restful ABAP Pr

使用electron开发一个h5的客户端应用创建http服务模拟后台接口mock

使用electron开发一个h5的客户端应用创建http服务模拟后端接口mock 在上一篇<electron快速开始>里讲述了如何快速的开始一个electron的应用程序,既然electron是可以直接使用node环境编写程序的,那么我们就可以有很多可以处理的事,比如我们可以做个可视化的前端构建工具,或者我们可以建一个后台的管理系统.基于这两天,我简单的做了一个模拟后端接口的http服务. 它可以完成如下功能: 它可以创建http本地服务 它可以模拟后端的多种请求方式,如get.post.pu

使用PHP开发一个简单的后台接口(响应移动端的get请求和post请求)

写一个简单的后台,在接到app请求数据的时候,返回对应的内容: index.php文件如下: <?php $id = $_POST['id']; if($id==001){ echo json_encode(array('id'=>001,'name'=>'zhangsan')); } if($id==002){ echo json_encode(array('id'=>002,'name'=>'lisi')); } if($id==003){ echo json_encod

struts+hibernate+jsp开发MedicineManager医疗后台管理系统源代码下载

原文:struts+hibernate+jsp开发MedicineManager医疗后台管理系统源代码下载 源代码下载地址:http://www.zuidaima.com/share/1550463451368448.htm MedicineManager医疗管理系统 代码没有建库脚本,会报错

为一个支持GPRS的硬件设备搭建一台高并发服务器用什么开发比较容易?

高并发服务器开发,硬件socket发送数据至服务器,服务器对数据进行判断,需要实现心跳以保持长连接. 同时还要接收另外一台服务器的消支付成功消息,接收到消息后控制硬件执行操作. 查了一些资料,java的netty,go,或者是用C/C++不知道该用哪个,想问一下哪个比较适合,学习更容易一些. 为一个支持GPRS的硬件设备搭建一台高并发服务器用什么开发比较容易? >> golang 这个答案描述的挺清楚的:http://www.goodpm.net/postreply/golang/101000