Как загрузить несколько файлов последовательно с помощью NSURLSession downloadTask в Swift

У меня есть приложение, которое должно загружать несколько больших файлов. Я хочу, чтобы он загружал каждый файл поочередно, а не одновременно. Когда он запускается одновременно, приложение становится перегруженным и сбой.

Итак. Im пытается обернуть 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)
    }

}

Как это можно правильно записать, чтобы достичь моей цели только загрузки одного файла за раз.

Ответ 1

Ваш код не будет работать, потому что 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 !isFinished { 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.

Ответ 2

Вот скорее минималистический и чисто быстрый подход. Без NSOperationQueue(), просто didSet-observer

    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"]
    }



}

Ответ 3

В фоновом режиме более одного кода. Я могу узнать, используя глобальную переменную и 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)
    }

таким образом, путем обновления progressview будет смотреть на количество загружаемых при каждом запуске таймера.

Ответ 4

Objective-C версия:

[operation2 addDependency:operation1]