如何在 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
失败,而不是 replaceError
和 nil
:
/// 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)
我有一个应用程序必须下载多个大文件。我希望它按顺序而不是同时一个一个地下载每个文件。当它同时运行时,应用程序会过载并崩溃。
所以。我试图将 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
失败,而不是 replaceError
和 nil
:
/// 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)