Как скачать несколько файлов последовательно с помощью NSURLSession downloadTask в Swift
У меня есть приложение, которое загружает несколько больших файлов. Я хочу, чтобы он загружал каждый файл поочередно, а не одновременно. При одновременном запуске приложение перегружается и вылетает.
Так. Я пытаюсь обернуть downloadTaskWithURL внутри NSBlockOperation, а затем установить maxConcurrentOperationCount = 1 в очереди. Я написал этот код ниже, но он не работал, так как оба файла загружаются одновременно.
import UIKit
class ViewController: UIViewController, NSURLSessionDelegate, NSURLSessionDownloadDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
processURLs()
}
func download(url: NSURL){
let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil)
let downloadTask = session.downloadTaskWithURL(url)
downloadTask.resume()
}
func processURLs(){
//setup queue and set max conncurrent to 1
var queue = NSOperationQueue()
queue.name = "Download queue"
queue.maxConcurrentOperationCount = 1
let url = NSURL(string: "http://azspeastus.blob.core.windows.net/azurespeed/100MB.bin?sv=2014-02-14&sr=b&sig=%2FZNzdvvzwYO%2BQUbrLBQTalz%2F8zByvrUWD%2BDfLmkpZuQ%3D&se=2015-09-01T01%3A48%3A51Z&sp=r")
let url2 = NSURL(string: "http://azspwestus.blob.core.windows.net/azurespeed/100MB.bin?sv=2014-02-14&sr=b&sig=ufnzd4x9h1FKmLsODfnbiszXd4EyMDUJgWhj48QfQ9A%3D&se=2015-09-01T01%3A48%3A51Z&sp=r")
let urls = [url, url2]
for url in urls {
let operation = NSBlockOperation { () -> Void in
println("starting download")
self.download(url!)
}
queue.addOperation(operation)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
//code
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
//
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
var progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
println(progress)
}
}
Как можно написать это правильно, чтобы достичь моей цели - скачивать только один файл за раз.
5 ответов
Ваш код не будет работать, потому что URLSessionDownloadTask
работает асинхронно Таким образом BlockOperation
завершается до завершения загрузки и, следовательно, пока операции запускаются последовательно, задачи загрузки будут выполняться асинхронно и параллельно.
Чтобы решить эту проблему, вы можете обернуть запросы в асинхронный Operation
подкласс. Посмотрите Конфигурирование Операций для Параллельного Выполнения в Руководстве по программированию Параллелизма для получения дополнительной информации.
Но прежде чем я покажу, как это сделать в вашей ситуации (на основе делегатов URLSession
), позвольте мне сначала показать вам более простое решение при использовании представления обработчика завершения. Позже мы будем опираться на это для вашего более сложного вопроса. Итак, в Swift 3 и позже:
class DownloadOperation : AsynchronousOperation {
var task: URLSessionTask!
init(session: URLSession, url: URL) {
super.init()
task = session.downloadTask(with: url) { temporaryURL, response, error in
defer { self.finish() }
guard let temporaryURL = temporaryURL, error == nil else {
print(error ?? "Unknown error")
return
}
do {
let manager = FileManager.default
let destinationURL = try manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(url.lastPathComponent)
try? manager.removeItem(at: destinationURL) // remove the old one, if any
try manager.moveItem(at: temporaryURL, to: destinationURL) // move new one there
} catch let moveError {
print("\(moveError)")
}
}
}
override func cancel() {
task.cancel()
super.cancel()
}
override func main() {
task.resume()
}
}
куда
/// Asynchronous operation base class
///
/// This is abstract to class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `Operation` subclass. You can subclass this and
/// implement asynchronous operations. All you must do is:
///
/// - override `main()` with the tasks that initiate the asynchronous task;
///
/// - call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `finish()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `finish()` is called.
class AsynchronousOperation: Operation {
/// State for this operation.
@objc private enum OperationState: Int {
case ready
case executing
case finished
}
/// Concurrent queue for synchronizing access to `state`.
private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)
/// Private backing stored property for `state`.
private var rawState: OperationState = .ready
/// The state of the operation
@objc private dynamic var state: OperationState {
get { return stateQueue.sync { rawState } }
set { stateQueue.sync(flags: .barrier) { rawState = newValue } }
}
// MARK: - Various `Operation` properties
open override var isReady: Bool { return state == .ready && super.isReady }
public final override var isExecuting: Bool { return state == .executing }
public final override var isFinished: Bool { return state == .finished }
// KVN for dependent properties
open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
if ["isReady", "isFinished", "isExecuting"].contains(key) {
return [#keyPath(state)]
}
return super.keyPathsForValuesAffectingValue(forKey: key)
}
// Start
public final override func start() {
if isCancelled {
finish()
return
}
state = .executing
main()
}
/// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception.
open override func main() {
fatalError("Subclasses must implement `main`.")
}
/// Call this function to finish an operation that is currently executing
public final func finish() {
if isExecuting { state = .finished }
}
}
Тогда вы можете сделать:
for url in urls {
queue.addOperation(DownloadOperation(session: session, url: url))
}
Так что это один очень простой способ обернуть асинхронный URLSession
/ NSURLSession
запросы в асинхронном режиме Operation
/ NSOperation
подкласс. В целом, это полезный шаблон, использующий AsynchronousOperation
завернуть какую-то асинхронную задачу в Operation
/ NSOperation
объект.
К сожалению, в вашем вопросе вы хотели использовать делегат на основе URLSession
/ NSURLSession
чтобы вы могли следить за ходом загрузок. Это сложнее.
Это потому, что "задача выполнена" NSURLSession
методы делегата вызываются у делегата объекта сеанса. Это яркая конструктивная особенность NSURLSession
(но Apple сделала это для упрощения фоновых сессий, что здесь не актуально, но мы застряли с этим ограничением дизайна).
Но мы должны асинхронно завершать операции по завершении задач. Таким образом, нам нужен какой-то способ, чтобы сессия выяснила, с какой операцией завершить didCompleteWithError
называется. Теперь у вас может быть каждая операция NSURLSession
объект, но оказывается, что это довольно неэффективно.
Итак, чтобы справиться с этим, я поддерживаю словарь, ключевой taskIdentifier
, который идентифицирует соответствующую операцию. Таким образом, когда загрузка заканчивается, вы можете "завершить" правильную асинхронную операцию. Таким образом:
/// Manager of asynchronous download `Operation` objects
class DownloadManager: NSObject {
/// Dictionary of operations, keyed by the `taskIdentifier` of the `URLSessionTask`
fileprivate var operations = [Int: DownloadOperation]()
/// Serial OperationQueue for downloads
private let queue: OperationQueue = {
let _queue = OperationQueue()
_queue.name = "download"
_queue.maxConcurrentOperationCount = 1 // I'd usually use values like 3 or 4 for performance reasons, but OP asked about downloading one at a time
return _queue
}()
/// Delegate-based `URLSession` for DownloadManager
lazy var session: URLSession = {
let configuration = URLSessionConfiguration.default
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
/// Add download
///
/// - parameter URL: The URL of the file to be downloaded
///
/// - returns: The DownloadOperation of the operation that was queued
@discardableResult
func queueDownload(_ url: URL) -> DownloadOperation {
let operation = DownloadOperation(session: session, url: url)
operations[operation.task.taskIdentifier] = operation
queue.addOperation(operation)
return operation
}
/// Cancel all queued operations
func cancelAll() {
queue.cancelAllOperations()
}
}
// MARK: URLSessionDownloadDelegate methods
extension DownloadManager: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
}
// MARK: URLSessionTaskDelegate methods
extension DownloadManager: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
let key = task.taskIdentifier
operations[key]?.urlSession(session, task: task, didCompleteWithError: error)
operations.removeValue(forKey: key)
}
}
/// Asynchronous Operation subclass for downloading
class DownloadOperation : AsynchronousOperation {
let task: URLSessionTask
init(session: URLSession, url: URL) {
task = session.downloadTask(with: url)
super.init()
}
override func cancel() {
task.cancel()
super.cancel()
}
override func main() {
task.resume()
}
}
// MARK: NSURLSessionDownloadDelegate methods
extension DownloadOperation: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let manager = FileManager.default
let destinationURL = try manager.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)
try? manager.removeItem(at: destinationURL)
try manager.moveItem(at: location, to: destinationURL)
} catch {
print(error)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
print("\(downloadTask.originalRequest!.url!.absoluteString) \(progress)")
}
}
// MARK: URLSessionTaskDelegate methods
extension DownloadOperation: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
defer { finish() }
if let error = error {
print(error)
return
}
// do whatever you want upon success
}
}
А затем используйте это так:
let downloadManager = DownloadManager()
override func viewDidLoad() {
super.viewDidLoad()
let urlStrings = [
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
]
let urls = urlStrings.compactMap { URL(string: $0) }
let completion = BlockOperation {
print("all done")
}
for url in urls {
let operation = downloadManager.queueDownload(url)
completion.addDependency(operation)
}
OperationQueue.main.addOperation(completion)
}
Смотрите историю изменений для реализации Swift 2.
Здесь довольно минималистичный и чисто быстрый подход. Без NSOperationQueue(), просто сделал Set-наблюдатель
import Foundation
class DownloadManager {
var delegate: HavingWebView?
var gotFirstAndEnough = true
var finalURL: NSURL?{
didSet{
if finalURL != nil {
if let s = self.contentOfURL{
self.delegate?.webView.loadHTMLString(s, baseURL: nil)
}
}
}
}
var lastRequestBeginning: NSDate?
var myLinks = [String](){
didSet{
self.handledLink = self.myLinks.count
}
}
var contentOfURL: String?
var handledLink = 0 {
didSet{
if handledLink == 0 {
self.finalURL = nil
print("")
} else {
if self.finalURL == nil {
if let nextURL = NSURL(string: self.myLinks[self.handledLink-1]) {
self.loadAsync(nextURL)
}
}
}
}
}
func loadAsync(url: NSURL) {
let sessionConfig = NSURLSessionConfiguration.ephemeralSessionConfiguration()
let session = NSURLSession(configuration: sessionConfig, delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
let request = NSMutableURLRequest(URL: url, cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData, timeoutInterval: 15.0)
request.HTTPMethod = "GET"
print("")
self.lastRequestBeginning = NSDate()
print("Requet began: \(self.lastRequestBeginning )")
let task = session.dataTaskWithRequest(request, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
if (error == nil) {
if let response = response as? NSHTTPURLResponse {
print("\(response)")
if response.statusCode == 200 {
if let content = String(data: data!, encoding: NSUTF8StringEncoding) {
self.contentOfURL = content
}
self.finalURL = url
}
}
}
else {
print("Failure: \(error!.localizedDescription)");
}
let elapsed = NSDate().timeIntervalSinceDate(self.lastRequestBeginning!)
print("trying \(url) takes \(elapsed)")
print(" Request finished")
print("____________________________________________")
self.handledLink -= 1
})
task.resume()
}
}
В ViewController:
protocol HavingWebView {
var webView: UIWebView! {get set}
}
class ViewController: UIViewController, HavingWebView {
@IBOutlet weak var webView: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let dm = DownloadManager()
dm.delegate = self
dm.myLinks = ["https://medium.com/the-mission/consider-the-present-and-future-value-of-your-decisions-b20fb72f5e#.a12uiiz11",
"https://medium.com/@prianka.kariat/ios-10-notifications-with-attachments-and-much-more-169a7405ddaf#.svymi6230",
"https://myerotica.com/jingle-bell-fuck-the-twins-5a48782bf5f1#.mjqz821yo",
"https://blog.medium.com/39-reasons-we-wont-soon-forget-2016-154ac95683af#.cmb37i58b",
"https://backchannel.com/in-2017-your-coworkers-will-live-everywhere-ae14979b5255#.wmi6hxk9p"]
}
}
Ответ Роба показал правильный способ сделать это. Я добился этого на основе делегата , чтобы отслеживать загрузку с представлением прогресса.
Вы можете проверить исходный код здесь.Множественная загрузка с индикатором выполнения (Github)
Более одного кода в фоновой ситуации. Я могу узнать, используя глобальную переменную и NSTimer. Вы тоже можете попробовать.
Определите глобальную переменную indexDownloaded.
import UIKit
import Foundation
private let _sharedUpdateStatus = UpdateStatus()
class UpdateStatus : NSObject {
// MARK: - SHARED INSTANCE
class var shared : UpdateStatus {
return _sharedUpdateStatus
}
var indexDownloaded = 0
}
Этот код добавьте в класс DownloadOperation.
print("⬇️" + URL.lastPathComponent! + " downloaded")
UpdateStatus.shared.indexDownloaded += 1
print(String(UpdateStatus.shared.indexDownloaded) + "\\" + String(UpdateStatus.shared.count))
Эта функция в вашем viewController.
func startTimeAction () {
let urlStrings = [
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
]
let urls = urlStrings.flatMap { URL(string: $0) }
for url in urls {
queue.addOperation(DownloadOperation(session: session, url: url))
}
UpdateStatus.shared.count = urls.count
progressView.setProgress(0.0, animated: false)
timer.invalidate()
timer = NSTimer.scheduledTimerWithTimeInterval(0.2, target: self, selector: #selector(timeAction), userInfo: nil, repeats: true)
}
func timeAction() {
if UpdateStatus.shared.count != 0 {
let set: Float = Float(UpdateStatus.shared.indexDownloaded) / Float(UpdateStatus.shared.count)
progressView.setProgress(set, animated: true)
}
таким образом, обновляя обзор прогресса будет смотреть на количество загрузок при каждом запуске таймера.