web-dev-qa-db-fra.com

Comment télécharger plusieurs fichiers séquentiellement à l'aide de NSURLSession downloadTask dans Swift

J'ai une application qui doit télécharger plusieurs fichiers volumineux. Je veux qu'il télécharge chaque fichier un par un séquentiellement au lieu de simultanément. Lorsqu'il s'exécute simultanément, l'application est surchargée et se bloque.

Donc. Im essayant d'encapsuler un downloadTaskWithURL dans un NSBlockOperation puis de définir le maxConcurrentOperationCount = 1 dans la file d'attente. J'ai écrit ce code ci-dessous mais cela n'a pas fonctionné car les deux fichiers sont téléchargés simultanément.

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)
    }

}

Comment peut-il écrire correctement pour atteindre mon objectif de ne télécharger qu'un seul fichier à la fois.

24
CraigH

Votre code ne fonctionnera pas car URLSessionDownloadTask s'exécute de manière asynchrone. Ainsi, BlockOperation se termine avant que le téléchargement ne soit terminé et donc pendant que les opérations se déclenchent séquentiellement, les tâches de téléchargement continueront de manière asynchrone et en parallèle.

Pour résoudre ce problème, vous pouvez encapsuler les demandes dans la sous-classe asynchrone Operation. Voir Configuration des opérations pour l'exécution simultanée dans le Guide de programmation des accès concurrents pour plus d'informations.

Mais avant d'illustrer comment procéder dans votre situation (le délégué URLSession), permettez-moi d'abord de vous montrer la solution la plus simple lors de l'utilisation du rendu du gestionnaire d'achèvement. Nous développerons plus tard cela pour votre question plus compliquée. Donc, dans Swift 3 et versions ultérieures:

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

Ensuite, vous pouvez faire:

for url in urls {
    queue.addOperation(DownloadOperation(session: session, url: url))
}

C'est donc un moyen très simple d'encapsuler les demandes asynchrones URLSession/NSURLSession dans la sous-classe asynchrone Operation/NSOperation. Plus généralement, il s'agit d'un modèle utile, utilisant AsynchronousOperation pour conclure une tâche asynchrone dans un objet Operation/NSOperation.

Malheureusement, dans votre question, vous vouliez utiliser les URLSession/NSURLSession basés sur les délégués pour pouvoir suivre la progression des téléchargements. C'est plus compliqué.

Cela est dû au fait que les méthodes de délégué "tâche terminée" NSURLSession sont appelées au niveau du délégué de l'objet de session. Il s'agit d'une caractéristique de conception exaspérante de NSURLSession (mais Apple l'a fait pour simplifier les sessions d'arrière-plan, ce qui n'est pas pertinent ici, mais nous sommes coincés avec cette limitation de conception).

Mais nous devons terminer les opérations de manière asynchrone à la fin des tâches. Nous avons donc besoin d'un moyen pour que la session comprenne que l'opération se termine lorsque didCompleteWithError est appelé. Maintenant, chaque opération pourrait avoir son propre objet NSURLSession, mais il s'avère que c'est assez inefficace.

Donc, pour gérer cela, je maintiens un dictionnaire, saisi par taskIdentifier de la tâche, qui identifie l'opération appropriée. De cette façon, une fois le téléchargement terminé, vous pouvez "terminer" l'opération asynchrone correcte. Donc:

/// 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
    }

}

Et puis utilisez-le comme ceci:

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)
}

Voir historique des révisions pour Swift 2 implémentation.

63
Rob

Voici une approche plutôt minimaliste et purement Swift. Sans NSOperationQueue (), juste 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()
        }
    }

Dans 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
user3567929

Plus d'un code dans la situation d'arrière-plan. Je peux apprendre par la variable globale utilisée et NSTimer. Vous pouvez aussi essayer.

Définissez la variable globale "indexDownloaded".

import UIKit
import Foundation

private let _sharedUpdateStatus = UpdateStatus()
class UpdateStatus : NSObject  {

// MARK: - SHARED INSTANCE
class var shared : UpdateStatus {
    return _sharedUpdateStatus
}
  var indexDownloaded = 0
}

Ce code s'ajoute à la classe DownloadOperation.

print("⬇️" + URL.lastPathComponent! + " downloaded")
        UpdateStatus.shared.indexDownloaded += 1
        print(String(UpdateStatus.shared.indexDownloaded) + "\\" + String(UpdateStatus.shared.count))

Cette fonction dans votre 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)
    }

de cette manière, en mettant à jour la progressview, vous regarderez le nombre de téléchargements à chaque exécution du timer.

1
Yunus T.