PhotoProgress/PhotoDownload.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
PhotoDownload supports NSProgressReporting and "downloads" a file URL. |
*/ |
import Foundation |
class PhotoDownload: NSObject, ProgressReporting { |
// MARK: Properties |
/// The URL to be downloaded. |
let downloadURL: URL |
/** |
The completionHandler is called once the download is finished with either |
the downloaded data, or an `NSError`. |
*/ |
var completionHandler: ((_ data: Data?, _ error: NSError?) -> Void)? |
let progress: Progress |
/// A class containing the fake parts of our download. |
fileprivate class DownloadState { |
/// The dispatch queue that all of our callbacks will be invoked on. |
var queue: DispatchQueue! |
/// The timer that drives the "download". |
var downloadTimer: DispatchSourceTimer? |
/// The error that our didFail callback should be called with. |
var downloadError: NSError? |
/// Whether or not we're paused. |
var isPaused = false |
} |
fileprivate var downloadState: DownloadState |
// MARK: Initializers |
init(URL: Foundation.URL) { |
downloadURL = (URL as NSURL).copy() as! Foundation.URL |
downloadState = DownloadState() |
progress = Progress() |
/* |
The progress starts out as indeterminate, since we don't know how many |
bytes there are to download yet. |
*/ |
progress.totalUnitCount = -1 |
/* |
Since our units are bytes, we use NSProgressKindFile so the NSProgress's |
localizedDescription and localizedAdditionalDescription return |
something nicer. |
*/ |
progress.kind = ProgressKind.file |
// We say we're a file operation so the localized descriptions are a little nicer. |
progress.setUserInfoObject(Progress.FileOperationKind.downloading as AnyObject, forKey: ProgressUserInfoKey.fileOperationKindKey) |
} |
/// Start the download. Can only be called once. |
func start() { |
assert(nil == downloadState.queue, "`downloadState.queue` must not be nil in \(#function).") |
// Fake a download. |
downloadState.queue = DispatchQueue(label: "download queue", attributes: []) |
downloadState.queue.async { |
do { |
// Fetch the data |
let data = try Data(contentsOf: self.downloadURL, options: []) |
// Our parameters for the "download". |
// Update every 0.5 seconds. |
let interval: Double = 0.5 |
// Bytes per second. |
let throughput: Double = 5000 |
// Create a timer |
let downloadTimer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags(rawValue: UInt(0)), queue: self.downloadState.queue) |
self.downloadState.downloadTimer = downloadTimer |
var downloadedBytes = 0 |
// Add a random delay to the start, to simulate latency. |
let randomMilliseconds = Int64(arc4random_uniform(500)) |
let delay = DispatchTime.now() + Double(Int64(interval * Double(NSEC_PER_SEC)) - Int64(randomMilliseconds * Int64(NSEC_PER_MSEC))) / Double(NSEC_PER_SEC) |
downloadTimer.scheduleRepeating(deadline: delay, interval: interval, leeway: .nanoseconds(0)) |
downloadTimer.setEventHandler { |
// Update the downloaded bytes. |
downloadedBytes += Int(throughput * interval) |
if downloadedBytes >= data.count { |
// We've finished! |
downloadTimer.cancel() |
return |
} |
// Call out that we've "downloaded" new data. |
self.didDownloadData(data, numberOfBytes: downloadedBytes) |
} |
downloadTimer.setCancelHandler { |
if downloadedBytes >= data.count { |
// Call out that we finished "downloading" data. |
self.didFinishDownload(data) |
} |
else { |
// Call out that we finished "downloading" data. |
self.didFailDownloadWithError(self.downloadState.downloadError!) |
} |
self.downloadState.downloadTimer = nil |
} |
// Call out that we will begin to "download" data. |
self.willBeginDownload(data.count) |
downloadTimer.resume() |
} |
catch let error { |
// Call out that we failed to "download" data. |
self.didFailDownloadWithError(error as NSError) |
} |
} |
} |
fileprivate func failDownloadWithError(_ error: NSError) { |
guard let downloadTimer = downloadState.downloadTimer else { return } |
downloadState.queue.async { |
/* |
Set the downloadError, then cancel. The timer's cancellation handler |
will invoke the fail callback with the error, if we haven't finished |
by then. |
*/ |
self.downloadState.downloadError = error |
// Resume the timer before cancelling it. |
if self.downloadState.isPaused { |
downloadTimer.resume() |
} |
downloadTimer.cancel() |
} |
} |
fileprivate func suspendDownload() { |
if let downloadTimer = downloadState.downloadTimer { |
downloadState.queue.async { |
// Do not suspend if we're already suspended, or if we're cancelled. |
guard !self.downloadState.isPaused && !downloadTimer.isCancelled else { return } |
// Simply suspend the timer. |
self.downloadState.isPaused = true |
downloadTimer.suspend() |
} |
} |
} |
fileprivate func resumeDownload() { |
if let downloadTimer = downloadState.downloadTimer { |
downloadState.queue.async { |
// Only resume if we're suspended and we're not cancelled. |
guard self.downloadState.isPaused && !downloadTimer.isCancelled else { return } |
// Simply resume the timer. |
downloadTimer.resume() |
self.downloadState.isPaused = false |
} |
} |
} |
fileprivate func callCompletionHandler(data: Data?, error: NSError?) { |
// Call the completion handler if we have one. |
completionHandler?(data, error) |
// Break any retain cycles by setting it to nil. |
completionHandler = nil |
} |
// Called when the "download" begins |
func willBeginDownload(_ downloadLength: Int) { |
progress.totalUnitCount = Int64(downloadLength) |
progress.isCancellable = true |
progress.cancellationHandler = { |
let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) |
self.failDownloadWithError(error) |
} |
progress.isPausable = true |
progress.pausingHandler = { |
self.suspendDownload() |
} |
progress.resumingHandler = { |
self.resumeDownload() |
} |
} |
/** |
Called periodically as the "download" occurs. data and numberOfBytes are |
aggregated values, and contain the entire download up to that point. |
*/ |
func didDownloadData(_ data: Data, numberOfBytes: Int) { |
progress.completedUnitCount = Int64(numberOfBytes) |
} |
/// Called when the "download" is completed. |
func didFinishDownload(_ downloadedData: Data) { |
progress.completedUnitCount = Int64(downloadedData.count) |
callCompletionHandler(data: downloadedData, error: nil) |
} |
/// Called if an error occurs during the "download" |
func didFailDownloadWithError(_ error: NSError) { |
callCompletionHandler(data: nil, error: error) |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-10-04