iOS-Android-私房菜iOS 开发 swift 文章收集

Swift:用NSURLSession下载iTunes歌曲

2016-05-18  本文已影响529人  厨子

在 swift 中使用 NSURLSession 时,看到了一篇 文章 使用 NSURLSession 从 iTunes 下载歌曲,也包含暂停、继续下载、模拟进度、取消下载的功能。但文章中一些技术细节稍微老旧了些,故,在这里重新整理一下,方便日后学习。
完整项目地址 TracksDownload-iTunes

准备工作
NSRULSession 简介

NSRULSession 在技术上不仅是一个类,而且也是一套处理基于 HTTP/HTTPS 请求的类。通过下图来了解一下它的构成

NSRULSession 是收、发 HTTP 请求的关键对象,它可以通过一个配置体 NSURLSessionConfiguration 创建。这个配置体可以设置 session 的超时时间,缓存策略,以及 HTTP headers ,它可以由三种方式创建:

NSRULSession 的所有的任务都需要关联一个任务 NSURLSessionTask对象,这个对象是任务的实际执行者,进行数据的获取,下载或上传文件。这个对象有三种类型:

你可以暂停、继续和取消一个任务。NSURLSessionDownloadTask 支持任务暂停,并在以后继续下载

一般地,NSURLSession 通过两种方式返回数据:一. 任务完成或失败后,通过一个 completionHandler 块返回数据;二. 在创建 session 时,指定一个代理方法,任务结束后通过回调方法返回数据

了解了 NSURLSession 的基本知识后,接下来开始实际操作

查询歌曲

要查询歌曲,需要借助 iTunes Search API ,在 UISearchBar 中,输入关键字,然后点击回车,进行搜索。

首先,在 SearchViewController.swift 中,在

var searchResults = [TrackModel]()

下面 添加以下代码:

// 歌曲查询 session 和 task
let session_queryTracks = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
var task_queryTracks: NSURLSessionTask?

然后,需要借助 UISearchBar 的代理方法 searchBarSearchButtonClicked(_:),来捕获用户的搜索行为。在 SearchViewController.swift 中找到这个代理方法,更新为如下代码:

func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    
    dismissKeyboard()

    let searchString = searchBar.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())

    if !searchString.isEmpty {
        // 1
        if task_queryTracks != nil {
            task_queryTracks?.cancel()
        }
        // 2   
        UIApplication.sharedApplication().networkActivityIndicatorVisible = true
        
        // 3 设置允许包含在搜索关键词中的字符
        let expectedCharSet = NSCharacterSet.URLQueryAllowedCharacterSet()
        let searchTerm = searchString.stringByAddingPercentEncodingWithAllowedCharacters(expectedCharSet)
        // 4
        let urlString = "http://itunes.apple.com/search?media=music&entity=song&term=\(searchTerm!)"
        let url = NSURL(string: urlString)
        // 5 生成查询任务对象
        task_queryTracks = session_queryTracks.dataTaskWithURL(url!, completionHandler: { [unowned self](data, response, error) in
            // 6
            dispatch_async(dispatch_get_main_queue(), {
                UIApplication.sharedApplication().networkActivityIndicatorVisible = false
            })
            // 7
            if let error = error {
                print(error.localizedDescription)
            }
            else if let httpResponse = response as? NSHTTPURLResponse {
            
                if httpResponse.statusCode == 200 {
                    self.updateSearchResults(data)
                }
            }
        })
        // 8 开始查询
        task_queryTracks?.resume()
    }
  }

按着上面的注释标号,依次说明一下:

运行 app,可以搜索任意一首歌,比如输入 Swift,回车搜索,会出现下图的效果:

准备下载歌曲

下载歌曲时,为了允许用户暂停、继续、取消下载,并且能显示下载进度,我们建立一个下载的 Model ,来保存下载状态。在 Model 文件夹下,新建类文件,命名为 DownloadModel 如图:

在文件 DownloadModel.swift 中,添加以下代码:

class DownloadModel {
    
    var downloadUrl: String
    var isDownloading = false
    var downloadProgress = 0.0
    
    var downloadTask: NSURLSessionDownloadTask?
    var downloadResumeData: NSData?
    
    init(downloadUrl: String) {
        self.downloadUrl = downloadUrl
    }
}

简单介绍一下这些属性:

建立下载任务

有了这个 Model 之后,为了追踪每一个下载任务,切换到 SearchViewController.swift 文件,找到

var searchResults = [TrackModel]()

在它下面一行,添加以下代码:

var trackDownload = [String: DownloadModel]()
lazy var session_downloadTracks: NSURLSession = {
        
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
        return session
    }()

接下来,来实现 NSURLSession 的代理方法。在文件 SearchViewController.swift 的最底部,加入以下代码:

extension SearchTracksViewController: NSURLSessionDownloadDelegate {
    
    func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
        
        print("下载结束")
    }
}

我们来触发下载任务。当用户点击 Download 按钮时,会调用方法 startDownload(_:),在此方法中执行下载任务,找到这个方法,更新为以下代码:

func startDownload(track: TrackModel) {
        
        if let urlString = track.trackPreviewUrl, url = NSURL(string:urlString) {
            
            let download = DownloadModel(downloadUrl: urlString)
            
            download.downloadTask = session_downloadTracks.downloadTaskWithURL(url)
            download.downloadTask?.resume()
            download.isDownloading = true
    
            trackDownload[urlString] = download
        }
    }

运行这个 app ,搜索任意一首歌,点击下载,过一会就会收到一条打印信息:"下载结束"。表示下载结束。

保存并播放歌曲

歌曲下载完之后,会调用方法 URLSession(_:downloadTask:didFinishDownloadingToURL:) 。方法里有个参数 URL,是文件的临时存放地址,我们要做到的就是把这个文件拷贝一个指定的地址(本地持久化)。然后,我们需要把已经完成的任务从字典 trackDownload 中移除,并更新相应的 tableViewCell。

为了方便找到对应的 cell,我们在 SearchViewController.swift 文件中添加一个辅助方法,用来返回 cell 所在的索引 index。代码如下:

func cellIndexOfDownloadTrack(downloadTrack:NSURLSessionDownloadTask) -> Int? {
        
        if let url = downloadTrack.originalRequest?.URL?.absoluteString {
            
            for (index, track) in searchResults.enumerate() {
                
                if url == track.trackPreviewUrl {
                    return index
                }
            }
        }
        return nil
    }

下一步就要开始把文件拷贝到我们指定的地址。更新代理方法 URLSession(_:downloadTask:didFinishDownloadingToURL:) 如下:

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
        
        // 1
        let originalURL: String? = downloadTask.originalRequest?.URL?.absoluteString
        if let url = originalURL, destinationURL = localFilePathForUrl(url) {
            
            print(destinationURL)
            
            // 2
            let fileManager = NSFileManager.defaultManager()
            do {
                try fileManager.removeItemAtURL(destinationURL)
            } catch {
                //
            }
            
            do {
                try fileManager.copyItemAtURL(location, toURL: destinationURL)
            } catch let error as NSError {
                print("Could not copy file to disk:\(error.localizedDescription)")
            }
        }
        
        // 3
        if let url = originalURL {
            
            trackDownload[url] = nil
            // 4
            if let index = cellIndexOfDownloadTrack(downloadTask) {
                dispatch_async(dispatch_get_main_queue(), {
                    
                    [unowned self] in
                    self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: index,inSection: 0)], withRowAnimation: .None)
                })
            }
        }
    }

对于上面代码标注的关键步骤,做一个简单说明:

运行 app,搜索一首歌,点击下载,稍等片刻就会收到一条打印信息:


下载按钮也会消失,点击已经下载的歌曲,就会弹出 MPMoviePlayerViewController 进行播放,如图:
模拟下载进度

模拟下载进度时,我们需要知道两点:

协议 NSURLSessionDownloadDelegate 的代理方法中,有一个方法带有我们需要的这两个参数,在文件 SearchViewController.swift 中,找到对这个协议的扩展,添加下面的方法:

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        
        // 1
        if let url = downloadTask.originalRequest?.URL?.absoluteString, trackDownload = trackDownload[url] {
            
            // 2
            trackDownload.downloadProgress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
            // 3
            let totalSize = NSByteCountFormatter.stringFromByteCount(totalBytesExpectedToWrite, countStyle: .Binary)
            // 4
            if let index = cellIndexOfDownloadTrack(downloadTask), trackCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0)) as? TrackCell {
                
                dispatch_async(dispatch_get_main_queue(), { 
                    trackCell.v_progress.progress = trackDownload.downloadProgress
                    trackCell.lb_progress.text = String(format: "%.1f%% of %@", trackDownload.downloadProgress*100,totalSize)
                })
            }
        }
    }

接下来一步步分析代码中的标注:

为了在 cell 上正确的显示下载状态,找到方法 tableView(_:cellForRowAtIndexPath:),在

let track = searchResults[indexPath.row]

下面添加代码:

        var showDownloadControls = false
        if let download = trackDownload[track.trackPreviewUrl!] {
            
            showDownloadControls = true
            cell.v_progress.progress = download.downloadProgress
            cell.lb_progress.text = download.isDownloading ? "Downloading..." : "Paused"
        }
        cell.v_progress.hidden = !showDownloadControls
        cell.lb_progress.hidden = !showDownloadControls

对于将要下载的歌曲,显示 “Downloading...”,暂停的显示 “Paused”,并且根据下载状态隐藏or显示 v_progress 和 lb_progress。对于正在下载的歌曲,下载按钮也要隐藏,所以,这句代码

cell.btn_download.hidden = trackHaveDownloaded

更新为

cell.btn_download.hidden = trackHaveDownloaded || showDownloadControls

运行 app,下载一首歌,看一下下载效果,如图所示:


暂停、继续、取消下载

......

暂停

......

暂停时,会产生恢复数据 resume data,根据这里面的数据,可以在以后继续下载,前提是服务器支持断点下载。

并且不是所有的条件下都可以继续下载的,具体哪些情况可以继续下载,请参考 文档

找到方法 pauseDownload(_:),更新为以下代码:

func pauseDownload(track: TrackModel) {
        
        if let url = track.trackPreviewUrl, download = trackDownload[url] {
            
            if download.isDownloading {
                download.downloadTask?.cancelByProducingResumeData({ (data) in
                    
                    if data != nil {
                        download.downloadResumeData = data
                    }
                })
                download.isDownloading = false
            }
        }
    }

上面的代码中,通过调用方法 cancelByProducingResumeData(_:),得到了 resume data,然后把这个 data 保存到相应的下载 Model 中,方便以后继续下载。并更新 Model 中的属性 isDownloading,表示停止下载。

......

继续

......

找到方法 resumeDownload(_:) ,更新为以下代码:

func resumeDownload(track: TrackModel) {
        
        if let previewUrl = track.trackPreviewUrl, download = trackDownload[previewUrl] {
            
            if let resumeData = download.downloadResumeData {
                
                download.downloadTask = session_downloadTracks.downloadTaskWithResumeData(resumeData)
                download.downloadTask!.resume()
                download.isDownloading = true
            }
            else if let url = NSURL(string: download.downloadUrl) {
                
                download.downloadTask = session_downloadTracks.downloadTaskWithURL(url)
                download.downloadTask!.resume()
                download.isDownloading = true
            }
        }
    }

在这个方法中,我们判断如果有 resume data,那么调用方法 downloadTaskWithResumeData(_:) 来继续下载。如果没有,就重新下载 。两种情况下,都更新下载状态为 true。

......

取消

......

取消下载就比较简单了,找到方法 cancelDownload(_:) ,更新为以下代码:

func cancelDownload(track: TrackModel) {
        
        if let url = track.trackPreviewUrl, download = trackDownload[url] {
            
            download.downloadTask?.cancel()
            trackDownload[url] = nil
        }
    }

在这个方法中,找到需要取消的下载任务,然后调用方法 cancel() 就会取消下载,并从字典中删掉这个任务。

最后要做的就是更新 cell 的工作了。回到方法 tableView(_:cellForRowAtIndexPath:) ,在 if 块中,添加下面的代码:

let title = download.isDownloading ? "Pause" : "Resume"
cell.btn_pause.setTitle(title, forState: .Normal)

cell.lb_progress.hidden = !showDownloadControls

下面添加以下代码:

cell.btn_pause.hidden = !showDownloadControls
cell.btn_cancel.hidden = !showDownloadControls

整个工作到此结束,运行 app,下载几首歌,并进行暂停,恢复,取消,效果如下图所示:

总结

在建立 DownloadModel 的时候,里面的 DownloadModel 最好是个 class 类型,而不要声明为 struct 类型,正如本项目中建立的一样。因为 struct 类型是 value typeclass 类型是 reference type

它们之间的区别请查看 Swift: 概念解释

本项目中,会对 DownloadModel 的对象所持有的属性,比如 isDownloading 等进行多次的修改。如果 DownloadModel 是 struct 类型,那么每次修改过之后,都需要再更新一遍字典 trackDownload 中对应的 model,因为 struct 类型的对象在传递的过程中,是重新拷贝一份的,拷贝后得到的数据并不指向原始地址。而 class 类型是 引用类型,故在传递过程中,这个对象都是指向原始地址的,对它的修改,也会影响原始数据。

我们可以对比一下 DownloadModl 为 class 类型和 struct 类型两种情况下,代码的差异性:

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        
        // 1
        if let url = downloadTask.originalRequest?.URL?.absoluteString, trackDownload = trackDownload[url] {
            
            // 2
            trackDownload.downloadProgress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
            // 3
            let totalSize = NSByteCountFormatter.stringFromByteCount(totalBytesExpectedToWrite, countStyle: .Binary)
            // 4
            if let index = cellIndexOfDownloadTrack(downloadTask), trackCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0)) as? TrackCell {
                
                dispatch_async(dispatch_get_main_queue(), { 
                    trackCell.v_progress.progress = trackDownload.downloadProgress
                    trackCell.lb_progress.text = String(format: "%.1f%% of %@", trackDownload.downloadProgress*100,totalSize)
                })
            }
        }
    }
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        
        // 1
        if let url = downloadTask.originalRequest?.URL?.absoluteString {
            
            download = trackDownload[url]! as DownloadModl
            // 2
            download.downloadProgress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
            // 3
            let totalSize = NSByteCountFormatter.stringFromByteCount(totalBytesExpectedToWrite, countStyle: .Binary)
            // 4
            if let index = cellIndexOfDownloadTrack(downloadTask), trackCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0)) as? TrackCell {
                
                dispatch_async(dispatch_get_main_queue(), { 
                    trackCell.v_progress.progress = download.downloadProgress
                    trackCell.lb_progress.text = String(format: "%.1f%% of %@", download.downloadProgress*100,totalSize)
                })
            }
           trackDownload[url] = download
        }
    }

注意区分上面两种情况下,使用 struct 类型会方便很多,不然,类似的还有方法 pauseDownload(_:)resumeDownload(_:)等,都需要做相应调整。

上一篇下一篇

猜你喜欢

热点阅读