如何在 Swift 中使用 NSURLSession downloadTask 顺序下载多个文件

How To Download Multiple Files Sequentially using NSURLSession downloadTask in 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)
    }

}

如何正确地写这个来实现我一次只下载一个文件的目标。

Objective-C 版本为:

[operation2 addDependency:operation1]

您的代码将无法运行,因为 URLSessionDownloadTask 运行 是异步的。因此 BlockOperation 在下载完成之前完成,因此当操作按顺序触发时,下载任务将异步和并行地继续。

虽然可以考虑一些变通方法(例如,递归模式在前一个请求完成后启动一个请求,后台线程上的非零信号量模式等),但优雅的解决方案是经过验证的异步解决方案之一框架。

在iOS 15及之后的版本中,我们将使用async-await方法download(from:delegate:),例如

func downloadFiles() async throws {
    let folder = try! FileManager.default
        .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)

    for url in urls {
        let (source, _) = try await URLSession.shared.download(from: url)
        let destination = folder.appendingPathComponent(url.lastPathComponent)
        try FileManager.default.moveItem(at: source, to: destination)
    }
}

在哪里

override func viewDidLoad() {
    super.viewDidLoad()

    Task {
        do {
            try await downloadFiles()
        } catch {
            print(error)
        }
    }
}

这仅适用于 iOS 15 及更高版本。但是 Xcode 13.2 及更高版本实际上允许您在 iOS 13 中使用异步等待,但您只需要编写自己的 async 格式的 download:

extension URLSession {
    @available(iOS, deprecated: 15, message: "Use `download(from:delegate:)` instead")
    func download(with url: URL) async throws -> (URL, URLResponse) {
        try await download(with: URLRequest(url: url))
    }

    @available(iOS, deprecated: 15, message: "Use `download(for:delegate:)` instead")
    func download(with request: URLRequest) async throws -> (URL, URLResponse) {
        let sessionTask = URLSessionTaskActor()

        return try await withTaskCancellationHandler {
            Task { await sessionTask.cancel() }
        } operation: {
            try await withCheckedThrowingContinuation { continuation in
                Task {
                    await sessionTask.start(downloadTask(with: request) { location, response, error in
                        guard let location = location, let response = response else {
                            continuation.resume(throwing: error ?? URLError(.badServerResponse))
                            return
                        }

                        // since continuation can happen later, let's figure out where to store it ...

                        let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
                            .appendingPathComponent(UUID().uuidString)
                            .appendingPathExtension(request.url!.pathExtension)

                        // ... and move it to there

                        do {
                            try FileManager.default.moveItem(at: location, to: tempURL)
                        } catch {
                            continuation.resume(throwing: error)
                            return
                        }

                        continuation.resume(returning: (tempURL, response))
                    })
                }
            }
        }
    }
}

private extension URLSession {
    actor URLSessionTaskActor {
        weak var task: URLSessionTask?

        func start(_ task: URLSessionTask) {
            self.task = task
            task.resume()
        }

        func cancel() {
            task?.cancel()
        }
    }
}

然后您会为 iOS 13 及更高版本调用此再现:

func downloadFiles() async throws {
    let folder = try! FileManager.default
        .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)

    for url in urls {
        let (source, _) = try await URLSession.shared.download(with: url)
        let destination = folder.appendingPathComponent(url.lastPathComponent)
        try FileManager.default.moveItem(at: source, to: destination)
    }
}

13之前的iOS版本,如果要控制一系列异步任务的并发度,我们会达到异步Operation子类。

或者,在 iOS 13 及更高版本中,您也可以考虑 Combine。 (还有其他第三方异步编程框架,但我会限制自己使用 Apple 提供的方法。)

这两个在我原来的回答中都有描述。


运算

为了解决这个问题,您可以将请求包装在异步 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 httpResponse = response as? HTTPURLResponse,
                200..<300 ~= httpResponse.statusCode
            else {
                // handle invalid return codes however you'd like
                return
            }

            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 emits all of the necessary KVO notifications 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 }
    
    // KVO 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) {
        guard
            let httpResponse = downloadTask.response as? HTTPURLResponse,
            200..<300 ~= httpResponse.statusCode
        else {
            // handle invalid return codes however you'd like
            return
        }

        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: [=18=]) }
    
    let completion = BlockOperation {
        print("all done")
    }
    
    for url in urls {
        let operation = downloadManager.queueDownload(url)
        completion.addDependency(operation)
    }

    OperationQueue.main.addOperation(completion)
}

参见 revision history 了解 Swift 2 实现。


合并

对于 Combine,我们的想法是为 URLSessionDownloadTask 创建一个 Publisher。然后你可以这样做:

var downloadRequests: AnyCancellable?

/// Download a series of assets

func downloadAssets() {
    downloadRequests = downloadsPublisher(for: urls, maxConcurrent: 1).sink { completion in
        switch completion {
        case .finished:
            print("done")

        case .failure(let error):
            print("failed", error)
        }
    } receiveValue: { destinationUrl in
        print(destinationUrl)
    }
}

/// Publisher for single download
///
/// Copy downloaded resource to caches folder.
///
/// - Parameter url: `URL` being downloaded.
/// - Returns: Publisher for the URL with final destination of the downloaded asset.

func downloadPublisher(for url: URL) -> AnyPublisher<URL, Error> {
    URLSession.shared.downloadTaskPublisher(for: url)
        .tryCompactMap {
            let destination = try FileManager.default
                .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                .appendingPathComponent(url.lastPathComponent)
            try FileManager.default.moveItem(at: [=19=].location, to: destination)
            return destination
        }
        .receive(on: RunLoop.main)
        .eraseToAnyPublisher()
}

/// Publisher for a series of downloads
///
/// This downloads not more than `maxConcurrent` assets at a given time.
///
/// - Parameters:
///   - urls: Array of `URL`s of assets to be downloaded.
///   - maxConcurrent: The maximum number of downloads to run at any given time (default 4).
/// - Returns: Publisher for the URLs with final destination of the downloaded assets.

func downloadsPublisher(for urls: [URL], maxConcurrent: Int = 4) -> AnyPublisher<URL, Error> {
    Publishers.Sequence(sequence: urls.map { downloadPublisher(for: [=19=]) })
        .flatMap(maxPublishers: .max(maxConcurrent)) { [=19=] }
        .eraseToAnyPublisher()
}

现在,不幸的是,Apple 提供了一个 DataTaskPublisher(它将整个资产加载到内存中,这对于大型资产来说是不可接受的解决方案),但可以参考 their source code 并对其进行调整以创建DownloadTaskPublisher:

//  DownloadTaskPublisher.swift
//
//  Created by Robert Ryan on 9/28/20.
//
//  Adapted from Apple's `DataTaskPublisher` at:
//  https://github.com/apple/swift/blob/88b093e9d77d6201935a2c2fb13f27d961836777/stdlib/public/Darwin/Foundation/Publishers%2BURLSession.swift

import Foundation
import Combine

// MARK: Download Tasks

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension URLSession {
    /// Returns a publisher that wraps a URL session download task for a given URL.
    ///
    /// The publisher publishes temporary when the task completes, or terminates if the task fails with an error.
    ///
    /// - Parameter url: The URL for which to create a download task.
    /// - Returns: A publisher that wraps a download task for the URL.

    public func downloadTaskPublisher(for url: URL) -> DownloadTaskPublisher {
        let request = URLRequest(url: url)
        return DownloadTaskPublisher(request: request, session: self)
    }

    /// Returns a publisher that wraps a URL session download task for a given URL request.
    ///
    /// The publisher publishes download when the task completes, or terminates if the task fails with an error.
    ///
    /// - Parameter request: The URL request for which to create a download task.
    /// - Returns: A publisher that wraps a download task for the URL request.

    public func downloadTaskPublisher(for request: URLRequest) -> DownloadTaskPublisher {
        return DownloadTaskPublisher(request: request, session: self)
    }

    public struct DownloadTaskPublisher: Publisher {
        public typealias Output = (location: URL, response: URLResponse)
        public typealias Failure = URLError

        public let request: URLRequest
        public let session: URLSession

        public init(request: URLRequest, session: URLSession) {
            self.request = request
            self.session = session
        }

        public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
            subscriber.receive(subscription: Inner(self, subscriber))
        }

        private typealias Parent = DownloadTaskPublisher
        private final class Inner<Downstream: Subscriber>: Subscription, CustomStringConvertible, CustomReflectable, CustomPlaygroundDisplayConvertible
        where
            Downstream.Input == Parent.Output,
            Downstream.Failure == Parent.Failure
        {
            typealias Input = Downstream.Input
            typealias Failure = Downstream.Failure

            private let lock: NSLocking
            private var parent: Parent?               // GuardedBy(lock)
            private var downstream: Downstream?       // GuardedBy(lock)
            private var demand: Subscribers.Demand    // GuardedBy(lock)
            private var task: URLSessionDownloadTask! // GuardedBy(lock)
            var description: String { return "DownloadTaskPublisher" }
            var customMirror: Mirror {
                lock.lock()
                defer { lock.unlock() }
                return Mirror(self, children: [
                    "task": task as Any,
                    "downstream": downstream as Any,
                    "parent": parent as Any,
                    "demand": demand,
                ])
            }
            var playgroundDescription: Any { return description }

            init(_ parent: Parent, _ downstream: Downstream) {
                self.lock = NSLock()
                self.parent = parent
                self.downstream = downstream
                self.demand = .max(0)
            }

            // MARK: - Upward Signals
            func request(_ d: Subscribers.Demand) {
                precondition(d > 0, "Invalid request of zero demand")

                lock.lock()
                guard let p = parent else {
                    // We've already been cancelled so bail
                    lock.unlock()
                    return
                }

                // Avoid issues around `self` before init by setting up only once here
                if self.task == nil {
                    let task = p.session.downloadTask(
                        with: p.request,
                        completionHandler: handleResponse(location:response:error:)
                    )
                    self.task = task
                }

                self.demand += d
                let task = self.task!
                lock.unlock()

                task.resume()
            }

            private func handleResponse(location: URL?, response: URLResponse?, error: Error?) {
                lock.lock()
                guard demand > 0,
                      parent != nil,
                      let ds = downstream
                else {
                    lock.unlock()
                    return
                }

                parent = nil
                downstream = nil

                // We clear demand since this is a single shot shape
                demand = .max(0)
                task = nil
                lock.unlock()

                if let location = location, let response = response, error == nil {
                    _ = ds.receive((location, response))
                    ds.receive(completion: .finished)
                } else {
                    let urlError = error as? URLError ?? URLError(.unknown)
                    ds.receive(completion: .failure(urlError))
                }
            }

            func cancel() {
                lock.lock()
                guard parent != nil else {
                    lock.unlock()
                    return
                }
                parent = nil
                downstream = nil
                demand = .max(0)
                let task = self.task
                self.task = nil
                lock.unlock()
                task?.cancel()
            }
        }
    }
}

现在,不幸的是,这不是使用 URLSession 委托模式,而是完成处理程序再现。但是可以想象,可以将其改编为委托模式。

此外,这将在下载失败时停止下载。如果你不希望它仅仅因为一个失败而停止,你可以想象将它定义为 Never 失败,而不是 replaceErrornil:

/// Publisher for single download
///
/// Copy downloaded resource to caches folder.
///
/// - Parameter url: `URL` being downloaded.
/// - Returns: Publisher for the URL with final destination of the downloaded asset. Returns `nil` if request failed.

func downloadPublisher(for url: URL) -> AnyPublisher<URL?, Never> {
    URLSession.shared.downloadTaskPublisher(for: url)
        .tryCompactMap {
            let destination = try FileManager.default
                .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                .appendingPathComponent(url.lastPathComponent)
            try FileManager.default.moveItem(at: [=21=].location, to: destination)
            return destination
        }
        .replaceError(with: nil)
        .receive(on: RunLoop.main)
        .eraseToAnyPublisher()
}

/// Publisher for a series of downloads
///
/// This downloads not more than `maxConcurrent` assets at a given time.
///
/// - Parameters:
///   - urls: Array of `URL`s of assets to be downloaded.
///   - maxConcurrent: The maximum number of downloads to run at any given time (default 4).
/// - Returns: Publisher for the URLs with final destination of the downloaded assets.

func downloadsPublisher(for urls: [URL], maxConcurrent: Int = 4) -> AnyPublisher<URL?, Never> {
    Publishers.Sequence(sequence: urls.map { downloadPublisher(for: [=21=]) })
        .flatMap(maxPublishers: .max(maxConcurrent)) { [=21=] }
        .eraseToAnyPublisher()
}

也许不用说,我通常不鼓励按顺序下载 assets/files。您应该允许它们并发 运行,但要控制并发程度,以免您的应用过载。上面列出的所有模式都将并发程度限制在合理的范围内。

这里是相当简约和纯粹的swift方法。没有 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://blog.medium.com/39-reasons-we-wont-soon-forget-2016-154ac95683af#.cmb37i58b",
                       "https://backchannel.com/in-2017-your-coworkers-will-live-everywhere-ae14979b5255#.wmi6hxk9p"]
    }



}

后台出现多码情况。我可以通过使用的全局变量和 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
}

此代码添加到下载操作中class。

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: [=12=]) }

    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会看到每次定时器运行时下载的数量。

Rob 的回答显示了执行此操作的正确方法。 我实现了 基于委托 的方式来跟踪带有进度视图的下载。

您可以在此处查看源代码。 Multiple download with progress bar (Github)