美文网首页
下载文件并显示进度

下载文件并显示进度

作者: 醉看红尘这场梦 | 来源:发表于2020-03-13 16:48 被阅读0次

我们通过一个很简单的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

相关文章

网友评论

      本文标题:下载文件并显示进度

      本文链接:https://www.haomeiwen.com/subject/sxtyjhtx.html