Ответ 1
Ваш код не будет работать, потому что URLSessionDownloadTask
работает асинхронно. Таким образом, BlockOperation
завершается до завершения загрузки, и, следовательно, пока операции запускаются последовательно, задачи загрузки будут выполняться асинхронно и параллельно.
Чтобы решить эту проблему, вы можете обернуть запросы в асинхронный подкласс Operation
. См. Настройка операций для одновременного выполнения в Руководстве по программированию параллелизма для получения дополнительной информации.
Но прежде чем я покажу, как это сделать в вашей ситуации (на основе делегатов URLSession
), позвольте мне сначала показать вам более простое решение при использовании представления обработчика завершения. Позже мы будем опираться на это для вашего более сложного вопроса. Итак, в Swift 3 и позже:
class DownloadOperation : AsynchronousOperation {
var task: URLSessionTask!
init(session: URLSession, url: URL) {
super.init()
task = session.downloadTask(with: url) { temporaryURL, response, error in
defer { self.finish() }
guard let temporaryURL = temporaryURL, error == nil else {
print(error ?? "Unknown error")
return
}
do {
let manager = FileManager.default
let destinationURL = try manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(url.lastPathComponent)
try? manager.removeItem(at: destinationURL) // remove the old one, if any
try manager.moveItem(at: temporaryURL, to: destinationURL) // move new one there
} catch let moveError {
print("\(moveError)")
}
}
}
override func cancel() {
task.cancel()
super.cancel()
}
override func main() {
task.resume()
}
}
Где
/// Asynchronous operation base class
///
/// This is abstract to class performs all of the necessary KVN of 'isFinished' and
/// 'isExecuting' for a concurrent 'Operation' subclass. You can subclass this and
/// implement asynchronous operations. All you must do is:
///
/// - override 'main()' with the tasks that initiate the asynchronous task;
///
/// - call 'completeOperation()' function when the asynchronous task is done;
///
/// - optionally, periodically check 'self.cancelled' status, performing any clean-up
/// necessary and then ensuring that 'finish()' is called; or
/// override 'cancel' method, calling 'super.cancel()' and then cleaning-up
/// and ensuring 'finish()' is called.
class AsynchronousOperation: Operation {
/// State for this operation.
@objc private enum OperationState: Int {
case ready
case executing
case finished
}
/// Concurrent queue for synchronizing access to 'state'.
private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)
/// Private backing stored property for 'state'.
private var rawState: OperationState = .ready
/// The state of the operation
@objc private dynamic var state: OperationState {
get { return stateQueue.sync { rawState } }
set { stateQueue.sync(flags: .barrier) { rawState = newValue } }
}
// MARK: - Various 'Operation' properties
open override var isReady: Bool { return state == .ready && super.isReady }
public final override var isExecuting: Bool { return state == .executing }
public final override var isFinished: Bool { return state == .finished }
// KVN for dependent properties
open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
if ["isReady", "isFinished", "isExecuting"].contains(key) {
return [#keyPath(state)]
}
return super.keyPathsForValuesAffectingValue(forKey: key)
}
// Start
public final override func start() {
if isCancelled {
finish()
return
}
state = .executing
main()
}
/// Subclasses must implement this to perform their work and they must not call 'super'. The default implementation of this function throws an exception.
open override func main() {
fatalError("Subclasses must implement 'main'.")
}
/// Call this function to finish an operation that is currently executing
public final func finish() {
if !isFinished { state = .finished }
}
}
Тогда вы можете сделать:
for url in urls {
queue.addOperation(DownloadOperation(session: session, url: url))
}
Так что один очень простой способ обернуть асинхронные запросы URLSession
/NSURLSession
в асинхронный подкласс Operation
/NSOperation
. В более общем плане это полезный шаблон, использующий AsynchronousOperation
для обертывания некоторой асинхронной задачи в объекте Operation
/NSOperation
.
К сожалению, в вашем вопросе вы хотели использовать основанные на делегатах URLSession
/NSURLSession
, чтобы вы могли следить за ходом загрузки. Это сложнее.
Это связано с тем, что методы делегата "задача выполнена" NSURLSession
вызываются в делегате объекта сеанса. Это ужасная особенность дизайна NSURLSession
(но Apple сделала это для упрощения фоновых сессий, что здесь не актуально, но мы застряли с этим ограничением дизайна).
Но мы должны асинхронно завершать операции по завершении задач. Таким образом, нам нужен какой-то способ, чтобы сессия могла определиться с завершением операции, когда вызывается didCompleteWithError
. Теперь у каждой операции может быть свой объект NSURLSession
, но оказывается, что это довольно неэффективно.
Поэтому, чтобы справиться с этим, я поддерживаю словарь, заданный задачей taskIdentifier
, который определяет соответствующую операцию. Таким образом, когда загрузка завершится, вы сможете "завершить" правильную асинхронную операцию. Таким образом:
/// Manager of asynchronous download 'Operation' objects
class DownloadManager: NSObject {
/// Dictionary of operations, keyed by the 'taskIdentifier' of the 'URLSessionTask'
fileprivate var operations = [Int: DownloadOperation]()
/// Serial OperationQueue for downloads
private let queue: OperationQueue = {
let _queue = OperationQueue()
_queue.name = "download"
_queue.maxConcurrentOperationCount = 1 // I'd usually use values like 3 or 4 for performance reasons, but OP asked about downloading one at a time
return _queue
}()
/// Delegate-based 'URLSession' for DownloadManager
lazy var session: URLSession = {
let configuration = URLSessionConfiguration.default
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
/// Add download
///
/// - parameter URL: The URL of the file to be downloaded
///
/// - returns: The DownloadOperation of the operation that was queued
@discardableResult
func queueDownload(_ url: URL) -> DownloadOperation {
let operation = DownloadOperation(session: session, url: url)
operations[operation.task.taskIdentifier] = operation
queue.addOperation(operation)
return operation
}
/// Cancel all queued operations
func cancelAll() {
queue.cancelAllOperations()
}
}
// MARK: URLSessionDownloadDelegate methods
extension DownloadManager: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
}
// MARK: URLSessionTaskDelegate methods
extension DownloadManager: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
let key = task.taskIdentifier
operations[key]?.urlSession(session, task: task, didCompleteWithError: error)
operations.removeValue(forKey: key)
}
}
/// Asynchronous Operation subclass for downloading
class DownloadOperation : AsynchronousOperation {
let task: URLSessionTask
init(session: URLSession, url: URL) {
task = session.downloadTask(with: url)
super.init()
}
override func cancel() {
task.cancel()
super.cancel()
}
override func main() {
task.resume()
}
}
// MARK: NSURLSessionDownloadDelegate methods
extension DownloadOperation: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let manager = FileManager.default
let destinationURL = try manager.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)
try? manager.removeItem(at: destinationURL)
try manager.moveItem(at: location, to: destinationURL)
} catch {
print(error)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
print("\(downloadTask.originalRequest!.url!.absoluteString) \(progress)")
}
}
// MARK: URLSessionTaskDelegate methods
extension DownloadOperation: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
defer { finish() }
if let error = error {
print(error)
return
}
// do whatever you want upon success
}
}
А потом используйте это так:
let downloadManager = DownloadManager()
override func viewDidLoad() {
super.viewDidLoad()
let urlStrings = [
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
]
let urls = urlStrings.compactMap { URL(string: $0) }
let completion = BlockOperation {
print("all done")
}
for url in urls {
let operation = downloadManager.queueDownload(url)
completion.addDependency(operation)
}
OperationQueue.main.addOperation(completion)
}
Смотрите историю изменений для реализации Swift 2.