我们通过一个很简单的app,向大家演示Alamofire开始、暂停、恢复和取消文件下载的功能。为了专注在我们的核心功能演示上,我们准备了一个项目模板
熟悉项目模板
打开Main.storyboard,可以看到我们的Demo App很简单:
image- 一个UILabel文字;
- 一个<key style="box-sizing: border-box;">UITextField</key>表示要下载的URL;
- 一个UIProgressView表示下载的进度;
- 一个UIStackView用于管理下载、暂停/恢复、以及取消按钮;
打开ViewController.swift,可以看到下面这些代码:
表示App当前状态的enum:
enum DownloadStatus {
case NotStarted
case Downloading
case Suspended
case Cancelled
}
class ViewController: UIViewController {
var currStatus = DownloadStatus.NotStarted
//... omit for simplicity
}
它们表达的含义很简单,当用户点击不同的按钮时,我们会设置ViewController里的currStatus属性。
用于连接UI控件的IBOutlet:
class ViewController: UIViewController {
var currStatus = DownloadStatus.NotStarted
@IBOutlet weak var downloadUrl: UITextField!
@IBOutlet weak var downloadProgress: UIProgressView!
@IBOutlet weak var beginBtn: UIButton!
@IBOutlet weak var suspendOrResumeBtn: UIButton!
@IBOutlet weak var cancelBtn: UIButton!
//... omit for simplicity
}
为了让代码便于管理,在ViewController.swift底部,我们把三个按钮的事件处理方法单独放在了ViewController的一个extension里:
extension ViewController {
// Enable download button when UITextField is not ""
@IBAction func valueChanged(sender: UITextField) {
print("text field: \(sender.text)")
if sender.text != "" {
self.beginBtn.enabled = true
}
else {
self.beginBtn.enabled = false
}
}
// Button actions
@IBAction func beginDownload(sender: AnyObject) {
print("Begin downloading...")
// TODO: Add begin downloading code here
self.suspendOrResumeBtn.enabled = true;
self.cancelBtn.enabled = true;
self.currStatus = .Downloading
}
@IBAction func suspendOrResumeDownload(sender: AnyObject) {
var btnTitle: String?
switch self.currStatus {
case .Downloading:
print("Suspend downloading...")
// TODO: Add suspending code here
self.currStatus = .Suspended
btnTitle = "Resume"
case .Suspended:
print("Resume downloading...")
// TODO: Add resuming code here
self.currStatus = .Downloading
btnTitle = "Suspend"
case .NotStarted, .Cancelled:
break
}
self.suspendOrResumeBtn.setTitle(btnTitle,
forState: UIControlState.Normal)
}
@IBAction func cancelDownload(sender: AnyObject) {
print("Cancel downloading...")
switch self.currStatus {
case .Downloading, .Suspended:
// TODO: Add cancel code here
self.currStatus = .Cancelled
self.cancelBtn.enabled = false
self.suspendOrResumeBtn.enabled = false
self.suspendOrResumeBtn.setTitle("Suspend",
forState: UIControlState.Normal)
default:
break
}
}
}
代码执行的逻辑很简单,第一个IBAction用于当<key style="box-sizing: border-box;">UITextField</key>不为空时,才启用Begin按钮。剩下的三个IBAction用于当点击不同的按钮时,向控制台打印不同的信息,并且设置App的当前状态。
同时,我们还在注释里添加了TODO。Alamofire的相关代码,稍后,我们就会添加在对应的TODO的位置。
为了简化代码,我们还通过extension给ViewController添加了两个computed properties,用于获取App documents和episodes目录的<key style="box-sizing: border-box;">NSURL</key>,稍后,我们的文件,就会下载到documents/episode目录里。
extension ViewController {
var documentsDirUrl: NSURL {
let fm = NSFileManager.defaultManager()
let url = fm.URLsForDirectory(
.DocumentDirectory,
inDomains: .UserDomainMask)[0]
return url
}
var episodesDirUrl: NSURL {
let url = self.documentsDirUrl
.URLByAppendingPathComponent(
"episodes",
isDirectory: true)
return url
}
}
最后,在ViewController的init方法里,如果episodeDirUrl指定的目录不存在,我们就手工创建一个:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
if !self.episodesDirUrl.checkResourceIsReachableAndReturnError(nil) {
try! NSFileManager.defaultManager()
.createDirectoryAtURL(self.episodesDirUrl,
withIntermediateDirectories: true,
attributes: nil)
}
}
以上就是这个模板项目的介绍,很简单。
image接下来,我们就来添加通过Alamofire下载文件的各种代码。
下载文件
我们把下载的代码,添加在beginDownload(sender: AnyObject)里:
// Button actions
@IBAction func beginDownload(sender: AnyObject) {
print("Begin downloading...")
// TODO: Add begin downloading code here
if let resUrl = self.downloadUrl.text {
Alamofire.download(.GET, resUrl, destination: dest)
}
self.suspendOrResumeBtn.enabled = true;
self.cancelBtn.enabled = true;
self.currStatus = .Downloading
}
Alamofire.download的前两个参数很好理解,.GET用于指定下载时使用的HTTP方法,resUrl用于指定要下载的资源。最后一个参数destination时什么呢?
在Alamofire的源代码里可以发现,它是一个Closure,返回一个<key style="box-sizing: border-box;">NSURL</key>:
public typealias DownloadFileDestination = (NSURL, NSHTTPURLResponse) -> NSURL
简单来说,Alamofire会把文件下载到App默认的tmp目录,之后,它需要调用一个Closure来获得从tmp目录把文件Copy走的位置,而指定这个位置,就是destination参数的作用。接下来,我们就来定义它:
// Button actions
@IBAction func beginDownload(sender: AnyObject) {
print("Begin downloading...")
// TODO: Add begin downloading code here
let dest: Request.DownloadFileDestination = { temporaryURL, response in
print(temporaryURL)
let pathComponent = response.suggestedFilename
let episodeUrl =
self.episodesDirUrl.URLByAppendingPathComponent(pathComponent!)
if episodeUrl.checkResourceIsReachableAndReturnError(nil) {
print("Clear the previous existing file.")
let fm = NSFileManager.defaultManager()
try! fm.removeItemAtURL(episodeUrl)
}
return episodeUrl
}
if let resUrl = self.downloadUrl.text {
Alamofire.download(.GET, resUrl, destination: dest)
}
self.suspendOrResumeBtn.enabled = true;
self.cancelBtn.enabled = true;
self.currStatus = .Downloading
}
在dest的定义里,有下面几点是值得说明的:
首先是它的两个参数:
- temporaryURL - 它是一个<key style="box-sizing: border-box;">NSURL</key>对象,表示Alamofire保存下载的临时文件的完整路径(稍后我们会在结果里看到它);
- response - 表示服务器返回的HTTP Response;
其次,在它的实现里,我们先使用:
let pathComponent = response.suggestedFilename
获取了要下载的文件名,Alamofire的suggestedFilename默认会使用下载URL中的最后一段做为文件名,当然,你也可以指定任意一个自己需要的名字。
然后,我们把文件名拼接在我们刚才创建的episodes目录的<key style="box-sizing: border-box;">NSURL</key>里,这样,我们就得到了一个iOS本地文件的完整<key style="box-sizing: border-box;">NSURL</key>对象:
let episodeUrl =
self.episodesDirUrl.URLByAppendingPathComponent(pathComponent!)
接下来,如果episodes目录存在同名文件,我们要先删掉它,否则Alamofire会在移动临时文件时返回错误:
if episodeUrl.checkResourceIsReachableAndReturnError(nil) {
print("Clear the previous existing file.")
let fm = NSFileManager.defaultManager()
try! fm.removeItemAtURL(episodeUrl)
}
最后,我们返回拼接的episodeUrl对象。这就是destination参数的一个简单实现。这样,Alamofire最基本的下载功能也就完成了,我们可以按Command + R编译执行。
输入我们要下载的链接,点击Begin按钮,就可以开始下载了:
image访问上图中的目录,我们就可以发现Alamofire在tmp目录(也就是dest closure中的temporaryURL)下载的临时文件,以及dest指定的目录保存的下载文件了:
image接下来,我们处理下载进度,让它显示在UIProgressView上。
处理下载进度
在beginDownload里,我们添加下面的代码:
@IBAction func beginDownload(sender: AnyObject) {
// Omit for simplicity...
if let resUrl = self.downloadUrl.text {
Alamofire.download(.GET, resUrl, destination: dest)
.progress { bytesRead, totalBytesRead, totalBytesExpectedToRead in
// This closure is NOT called on the main queue for performance
// reasons. To update your ui, dispatch to the main queue.
dispatch_async(dispatch_get_main_queue()) {
let progress =
Float(totalBytesRead) / Float(totalBytesExpectedToRead)
self.downloadProgress.progress = progress
}
}
}
// Omit for simplicity...
}
简单起见,我们去掉了beginDownload前后不相关的代码。Alamofire.download会返回一个Alamofire.Request对象,我们调用它的progress方法获得下载进度通知。progress方法接受一个Closure参数,这个Closure自身带有三个类型是Int64类型的参数:
- bytesRead - 表示一个HTTP包传输的字节数;
- totalBytesRead - 表示已经下载的总字节数;
- bytesRead - 表示要下载文件的总大小;
为了不卡住我们的界面,progress方法并不会在App的主线程中执行。为了更新UIProgressView,我们手工把更新UI的代码放到主线程中:
// This closure is NOT called on the main queue for performance
// reasons. To update your ui, dispatch to the main queue.
dispatch_async(dispatch_get_main_queue()) {
let progress = Float(totalBytesRead) / Float(totalBytesExpectedToRead)
self.downloadProgress.progress = progress
}
至于更新UIProgressView则很简单,我们只要更新它的progress属性就可以了。
完成之后,按Command + R重新编译执行,输入要下载的URL,点击begin按钮,就可以正常显示下载进度了。
image
网友评论