How to aggregate multiple child Progress into one master Progress in Swift - ios

I'm trying to download multiple files simultaneously using NSURLSession in Swift. I want to merge all the download progress status into one as to show 100% when all the files are downloaded. Currently I get 100% for each file download completion, but I need 100% only if all the files are downloaded. How can I achieve this in Swift ?
Here is my DownloadManager Class :
class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
static var shared = DownloadManager()
var task1Progress = 0.00
var task2Progress = 0.00
typealias ProgressHandler = (Float) -> ()
var onProgress : ProgressHandler? {
didSet {
if onProgress != nil {
let _ = activate()
}
}
}
override private init() {
super.init()
}
func activate() -> URLSession {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
// Warning: If an URLSession still exists from a previous download, it doesn't create a new URLSession object but returns the existing one with the old delegate object attached!
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
func calculateProgress(session : URLSession, completionHandler : #escaping (Float) -> ()) {
session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
let progress = downloads.map({ (task) -> Float in
if task.countOfBytesExpectedToReceive > 0 {
return Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive)
} else {
return 0.0
}
})
completionHandler(progress.reduce(0.0, +))
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
if let onProgress = onProgress {
calculateProgress(session: session, completionHandler: onProgress)
}
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
debugPrint("Download Progress \(downloadTask) \(progress)")
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
debugPrint("Download finished: \(location)")
try? FileManager.default.removeItem(at: location)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
debugPrint("Task completed: \(task), error: \(String(describing: error))")
}
private func calculateProgress(session : URLSession, completionHandler : #escaping (Float) -> ()) {
session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
let progress = downloads.map({ (task) -> Float in
if task.countOfBytesExpectedToReceive > 0 {
if (task.taskIdentifier == 1) {
self.task1Progress = Double(Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive))
} else if (task.taskIdentifier == 2){
self.task2Progress = Double(Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive))
}
print("pro1 = \(self.task1Progress) pro2 = \(self.task2Progress)")
if(self.task1Progress>0.0000 && self.task2Progress>0.000) {
return Float(min(self.task1Progress ,self.task2Progress))
}
return Float(max(self.task1Progress ,self.task2Progress))
} else {
return 0.0
}
})
completionHandler(progress.reduce(0.0, +))
}
}
}

You can use dispatchgroup tp achieve the desired behaviour.
To download file you will call shared method of downloadManager. Dispatch the downloading operation on dispatchGroup and in notify you will get the callback when all files are downloaded.
Please find example below :
func dispatchGroupUsage (completion: CallBack) {
let backgroundQ = DispatchQueue.global(attributes: .qosDefault)
let group = DispatchGroup()
for number in numberOfFilesToBeDownloaded {
group.enter()
backgroundQ.async(group: group, execute: {
// call download manager's method to download files
group.leave()
})
}
group.notify(queue: DispatchQueue.main, execute: {
print("All Done"); completion(result: fill)
})
}

variables for store data and calculate progress
var progress: Float = 0
var expectedContentLength: Int64 = 0
var allData: Data = Data()
create default session:
let defaultConfiguration = URLSessionConfiguration.default
let defaultSession = URLSession(configuration: defaultConfiguration, delegate: self, delegateQueue: nil
download data from url you can call this method as many times as you want
func downlod(from url: URL, session: URLSession) {
let dataTask = session.dataTask(with: url)
dataTask.resume();
}
and you need to implement following delegates
URLSessionDelegate, URLSessionDataDelegate
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Swift.Void) {
progress = 0
expectedContentLength += response.expectedContentLength
completionHandler(.allow)
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
allData.append(data)
progress = Float(allData.count) / Float(expectedContentLength)
print("progress - \(progress)")
}
don't forget to reset expectedContentLength variable when you starting download, like this
expectedContentLength = 0

I had a similar requirement and this is how I managed to fix, for me I had a list of topics and each topic had chapters and each chapter was a file. So when a topic is downloaded, I had to download its chapters. So here is the catch lets say a topic had 15 files ( I get the number from metadata), there will be 15 download objects and 15 download taskes, whenever each task fires the progress I handle it in the below function in my view.
The idea is simple each file start from 0.0 to 1.0 to finish, so when all 15 finishes the sum of all progress would be 15.0 that means all download gets finished when the sum of progress == total number of files
func handleProgress(percentage:Float){
// Array of files
let totalFileCount = downloadData.count
totalPercentageProgress += percentage
DispatchQueue.main.async {
self.downloadProgressView.progress = self.totalPercentageProgress / Float(totalFileCount)
}
}
-----
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64){
if totalBytesExpectedToWrite > 0 {
let progressPercentage = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
let progressHandler = getProgressHandlerForTask(identifier: downloadTask.taskIdentifier)
print(progressPercentage)
delegate?.handleProgress(progressPercentage)
}
}

Just use NSProgress. It allows you to do exactly what you want.
Each NSURLSessionTask has a progress property (if you are targetting an older OS you can create one yourself).
Then just create a parent NSProgress instance and add each task progress as a child.
Finally, observe the fractionCompleted property of NSProgress and update your progress indicator.

Related

Swift Mjpeg Streaming Only Showing Single Frame

I am trying to create a Mjepg stream. I have followed a tutorial and it's not a lot of code to get a stream to lead but I don't understand why it's not working for me.
//Class properties
private var mjpegSession : URLSession?
private var mjpegData:Data = Data()
override func viewDidLoad(){
...
mjpegSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
if let url = URL(string: "http://cam6284208.miemasu.net/nphMotionJpeg?Resolution=640x480") {
mjpegSession?.dataTask(with: url).resume()
}
}
extension ViewController: URLSessionDataDelegate, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
mjpegData.append(data)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
if mjpegData.count > 0 {
DispatchQueue.main.async{
if let image = UIImage(data: self.mjpegData){
self.cameraImageView.image = image
}
}
}
mjpegData.removeAll()
completionHandler(.allow)
}
}
The tutorial removed all the data in on the line that says
mjpegData.removeAll()
However if I do that the image is incomplete and there's errors in the log about not being able to decode an image. If I remove it, I get a complete frame or image but I only get one. Using that url in VLC shows a constant stream so I know the url is valid stream. Thanks for everyone's time.
So I found a solution having to do with queues and background threads that seemed to fix my problem. Here's the solution in case anyone else runs into this.
//Class properties
private var mjpegSession : URLSession?
private var mjpegData:Data = Data()
override func viewDidLoad(){
...
self.queue = DispatchQueue(label: "MjpegQueue")
self.mjpegSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
if let url = URL(string: "http://cam6284208.miemasu.net/nphMotionJpeg?Resolution=640x480") {
DispatchQueue.global().async {
self.mjpegSession?.dataTask(with: url).resume()
}
}
}
extension ViewController: URLSessionDataDelegate, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
mjpegData.append(data)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
queue.sync{
if mjpegData.count > 0 {
let data = self.mjpegData
if let image = UIImage(data: data){
DispatchQueue.main.async {
self.cameraImageView.image = image
}
}
}
mjpegData.removeAll()
}
completionHandler(.allow)
}
}
import UIKit
class ViewController
: UIViewController, URLSessionDataDelegate
{
// Variables
#IBOutlet weak var imageview : UIImageView!
private var queue = Data( )
// View : Did Load
override func viewDidLoad( )
{
super.viewDidLoad()
let session = URLSession( configuration: .default, delegate: self, delegateQueue: nil )
if let url = URL( string: "http://cam6284208.miemasu.net/nphMotionJpeg?Resolution=640x480" )
{
session.dataTask( with: url ).resume( )
}
}
// URLSession : Data Callback
func urlSession
(
_ session : URLSession,
dataTask : URLSessionDataTask,
didReceive data : Data
)
{
queue.append( data )
}
// URLSession : Response Callback
func urlSession
(
_ session : URLSession,
dataTask : URLSessionDataTask,
didReceive response : URLResponse,
completionHandler : #escaping( URLSession.ResponseDisposition ) -> Void
)
{
if queue.count > 0
{
if let image = UIImage( data: queue )
{
DispatchQueue.main.async
{
self.imageview.image = image
}
}
}
queue.removeAll( )
completionHandler( .allow )
}
}

URLSession downloadTask(with:) creates multiple file downloads

Background: I have a page where users can go to download various map files. I currently have a solution setup but it has some issues. Here is what I have:
public class FileDownloader: NSObject, URLSessionDownloadDelegate {
public let identifier: String
public let progress = CurrentValueSubject<Float, Never>(0)
public let handleFinish: (URL) -> Void
public let handleError: (Error?) -> Void
public init(identifier: String, handleFinish: #escaping (URL) -> Void, handleError: #escaping (Error?) -> Void) {
self.identifier = identifier
self.handleFinish = handleFinish
self.handleError = handleError
}
lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: self.identifier)
config.sessionSendsLaunchEvents = true
let queue = OperationQueue()
return URLSession(configuration: config, delegate: self, delegateQueue: queue)
}()
func startDownload(url: URL) {
let backgroundTask = urlSession.downloadTask(with: url)
backgroundTask.resume()
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
self.handleError(error)
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
self.handleFinish(location)
}
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let percentage = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
self.progress.send(percentage)
}
}
Basically I create a new FileDownloader for each row file. The problems are the following:
On the simulator, I start a download and let it run for a bit, then I stop the app. When I start it again and try to download the file again, it starts downloading 2 of the same file and updates me with the progress of both.
I'm not sure how to get the current progress when I go away from the download page and then come back.
Seems like if I could get the currently running task then I could use that. Is it possible to query the system for a running download and start getting the progress updates? Am I way off on my implementation and need to make changes to do what I want to do?

iOS swift notficiation after download finishes using URLSession

I am downloading a long file using URLSession and after the download is finished i am trying to show a notification to the user to let him know that the download is completed.
Notification works perfectly when the app is running. But not working when the app goes background. When the app comes to foreground again then the notification code starts running.
My codes:
import Foundation
import Zip
import UserNotifications
class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
static var shared = DownloadManager()
var selectedBook: Book!
typealias ProgressHandler = (Float, Float, Float) -> ()
var onProgress : ProgressHandler? {
didSet {
if onProgress != nil {
let _ = activate()
}
}
}
override private init() {
super.init()
}
func activate() -> URLSession {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
// Warning: If an URLSession still exists from a previous download, it doesn't create a new URLSession object but returns the existing one with the old delegate object attached!
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
private func calculateProgress(session : URLSession, completionHandler : #escaping (Float, Float, Float) -> ()) {
session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
let progress = downloads.map({ (task) -> Float in
if task.countOfBytesExpectedToReceive > 0 {
return Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive)
} else {
return 0.0
}
})
let countOfBytesReceived = downloads.map({ (task) -> Float in
return Float(task.countOfBytesReceived)
})
let countOfBytesExpectedToReceive = downloads.map({ (task) -> Float in
return Float(task.countOfBytesExpectedToReceive)
})
if progress.reduce(0.0, +) == 1.0 {
self.postNotification()
}
completionHandler(progress.reduce(0.0, +), countOfBytesReceived.reduce(0.0, +), countOfBytesExpectedToReceive.reduce(0.0, +))
}
}
func postUnzipProgress(progress: Double) {
NotificationCenter.default.post(name: .UnzipProgress, object: progress)
}
func postNotification() {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
// Enable or disable features based on authorization.
}
let content = UNMutableNotificationContent()
content.title = NSString.localizedUserNotificationString(forKey: "Download Completed", arguments: nil)
content.body = NSString.localizedUserNotificationString(forKey: "Quran Touch app is ready to use", arguments: nil)
content.sound = UNNotificationSound.default()
content.categoryIdentifier = "com.qurantouch.qurantouch"
// Deliver the notification in 60 seconds.
let trigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 2.0, repeats: false)
let request = UNNotificationRequest.init(identifier: "downloadCompleted", content: content, trigger: trigger)
// Schedule the notification.
center.add(request)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
if let onProgress = onProgress {
calculateProgress(session: session, completionHandler: onProgress)
}
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
debugPrint("Progress \(downloadTask) \(progress)")
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
debugPrint("Download finished: \(location)")
let folder = URL.createFolder(folderName: selectedBook.folder)
let fileURL = folder!.appendingPathComponent("ClipsAndTacksF1ForModeler.zip")
if let url = URL.getFolderUrl(folderName: selectedBook.folder) {
do {
try FileManager.default.moveItem(at: location, to: fileURL)
try Zip.unzipFile((fileURL), destination: url, overwrite: true, password: nil, progress: { (progress) -> () in
self.postUnzipProgress(progress: progress)
if progress == 1 {
// self.postNotification()
UserDefaults.standard.set("selected", forKey: self.selectedBook.fileKey)
URL.removeFile(file: fileURL)
}
}, fileOutputHandler: {(outputUrl) -> () in
})
} catch {
print(error)
}
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
debugPrint("Task completed: \(task), error: \(error)")
}
}
And started the download here
func downloadBookWithUrl(url: String) {
DownloadManager.shared.selectedBook = selectedBook
let url = URL(string: url)!
let task = DownloadManager.shared.activate().downloadTask(with: url)
task.resume()
}
I got an example from apple that is written in Objective C. But couldn't get through it as i don't know Objective C.
Here is the example: https://developer.apple.com/library/archive/samplecode/SimpleBackgroundTransfer/Introduction/Intro.html#//apple_ref/doc/uid/DTS40013416
I followed the instruction from Apple docs suggested by subdan in comments
Implented the handleEventsForBackgroundURLSession method in appDelegate. And before showing notification, I called the completion handler from appDelegate and it started working.
In appDelegate:
var backgroundCompletionHandler: (() -> Void)?
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: #escaping () -> Void) {
backgroundCompletionHandler = completionHandler
}
And before calling the notification :
DispatchQueue.main.async {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let backgroundCompletionHandler =
appDelegate.backgroundCompletionHandler else {
return
}
backgroundCompletionHandler()
self.postNotification()
}

Swift: How to catch disk full error on background URLSession.downloadTask?

I'm struggling to understand what I thought would be easy.
I have a URLSession.downloadTask. I have set my downloading object as the URLSession delegate and the following delegate methods do receive calls, so I know my delegate is set correctly.
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?)
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
The case I can't trap is when the downloadTask fills up the disk space on the iPad. None of those delegate methods get called.
How should I catch this error?
Here's my download object:
import Foundation
import Zip
import SwiftyUserDefaults
extension DownloadArchiveTask: URLSessionDownloadDelegate {
// Updates progress info
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
self.delegate?.updateProgress(param: progress)
}
// Stores downloaded file
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("DownloadArchiveTask: In didFinishDownloadingTo")
}
}
extension DownloadArchiveTask: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("DownloadArchiveTask: In didCompleteWithError")
if error != nil {
print("DownloadArchiveTask: has error")
self.delegate?.hasDiskSpaceIssue()
}
}
}
extension DownloadArchiveTask: URLSessionDelegate {
// Background task handling
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
print("DownloadArchiveTask: In handler for downloading archive")
DispatchQueue.main.async {
let sessionIdentifier = session.configuration.identifier
if let sessionId = sessionIdentifier, let app = UIApplication.shared.delegate as? AppDelegate, let handler = app.completionHandlers.removeValue(forKey: sessionId) {
handler()
}
}
}
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
print("DownloadArchiveTask: didBecomeInvalidWithError")
if error != nil {
print("DownloadArchiveTask: has error")
self.delegate?.hasDiskSpaceIssue()
}
}
}
class DownloadArchiveTask: NSObject {
var delegate: UIProgressDelegate?
var archiveUrl:String = "http://someurl.com/archive.zip"
var task: URLSessionDownloadTask?
static var shared = DownloadArchiveTask()
// Create downloadsSession here, to set self as delegate
lazy var session: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background_archive")
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
func initialDownload() {
// Create URL to the source file you want to download
let fileURL = URL(string: archiveUrl)
let request = URLRequest(url:fileURL!)
self.task = self.session.downloadTask(with: request)
task?.resume()
}
}
Anyone done this before? I can't believe it's this hard - I must be approaching the problem in the wrong way...
I had to solve this problem for my company not that long ago. Now my solution is in Objective C so you'll have to convert it over to Swift, which shouldn't be that hard. I created a method that got the available storage space left on the device and then checked that against the size of the file we're downloading. My solution assumes you know the size of the file you download, in your case you can use the totalBytesExpectedToWrite parameter in the didWriteData method.
Here's what I did:
+ (unsigned long long)availableStorage
{
unsigned long long totalFreeSpace = 0;
NSError* error = nil;
NSArray* paths = NSSearchPathForDirectoriesInDomain(NSDocumentDirectory, NSUserDomainMask, YES);
NSDictionary* dictionary = [[NSFileManager defaultManager] attributesOfFileSystemForPath:[paths lastObject] error:&error];
if (dictionary)
{
NSNumber* freeFileSystemSizeInBytes = [dictionary objectForKey:NSFileSystemFreeSize];
totalFreeSpace = [freeFileSystemSizeInBytes unsignedLongLongValue];
}
return totalFreeSpace;
}
Make sure you leave some room for error with these numbers, since iTunes, the settings app on the device, and this number never match up. The number we get here is the smallest of the three and is in MB. I hope this helps and let me know if you need help converting it to Swift.

Download with URLSession and Operation queue does not start next operation when application is in background

Use case : Download file from a server via one API which will give the download server url(download server url vality is for 10 secs only).
I have enabled the background capabilities.
Then created one Download Manager which holds the OperationQueue for download data task.
In Operation class when main() function calls, I have makes the API call which will return the download server URL and started a download.
Works fine in debug mode but does not work in release mode.
Download Manager :
final class SCDownloadManager: NSObject {
#objc static var shared = SCDownloadManager()
private override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(SCDownloadManager.networkDidChange(_:)), name: NSNotification.Name.init(NetworkConnectionChanged), object: nil)
}
/// Dictionary of operations, keyed by the `Download URL` of the `URLSessionTask`
fileprivate var operations = [URL: SCDownloadOperation]()
fileprivate var serverURLRequestMap = [URL: SCDownloadOperation]()
/// Serial NSOperationQueue for downloads
private let queue: OperationQueue = {
let _queue = OperationQueue()
_queue.name = "SCDownloadManagerQueue"
_queue.maxConcurrentOperationCount = 3
return _queue
}()
/// Delegate-based NSURLSession for DownloadManager
lazy var downloadSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.xyz.background.download")
let sessionQueue = OperationQueue()
return URLSession(configuration: configuration, delegate: self, delegateQueue: sessionQueue)
}()
lazy var dataSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.xyz.background.url")
let sessionQueue = OperationQueue()
return URLSession(configuration: configuration, delegate: self, delegateQueue: sessionQueue)
}()
#discardableResult
#objc func addDownload(_ downloadRecord: DownloadRecord) -> SCDownloadOperation {
let operation = SCDownloadOperation(withDownloadRecord: downloadRecord)
operation.delegate = self
operations[downloadRecord.downloadUrl] = operation
queue.addOperation(operation)
return operation
}
/// Cancel all queued operations
#objc func cancelAll() {
queue.cancelAllOperations()
}
#objc func networkDidChange(_ notification: Notification) {
if let reachability = notification.object as? Reachability {
if reachability.currentReachabilityStatus() == NotReachable {
cancelAll()
}
}
}
}
//MARK:- Download operation delegate
extension SCDownloadManager: SCDownloadOperationDelegate {
func getServerDownloadUrl(forRequest request: URLRequest, ofOperation operation: SCDownloadOperation) {
if let url = request.url {
serverURLRequestMap[url] = operation
}
self.dataSession.downloadTask(with: request).resume()
}
}
// MARK: URLSessionDownloadDelegate methods
extension SCDownloadManager: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let sourceURL = downloadTask.originalRequest?.url else { return }
if session == dataSession {
if FileManager.default.isReadableFile(atPath: location.path) {
if let data = FileManager.default.contents(atPath: location.path) {
if let downloadOperation = serverURLRequestMap[sourceURL] {
if let serverUrl = String(data: data, encoding: .utf8), let downloadServerUrl = URL(string: serverUrl) {
self.operations.changeKey(from: downloadOperation.downloadRecord.downloadUrl, to: downloadServerUrl)
downloadOperation.downloadRecord.downloadUrl = downloadServerUrl
if let url = downloadOperation.downloadRecord.downloadUrl {
downloadOperation.downloadRecord.downloadTask = downloadSession.downloadTask(with: url)
downloadOperation.downloadRecord.downloadTask.resume()
}
}
}
do {
try FileManager.default.removeItem(at: location)
} catch {
print("error while removing file from default location")
}
}
}
} else {
operations[sourceURL]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if session == self.downloadSession {
guard let sourceURL = downloadTask.originalRequest?.url else { return }
operations[sourceURL]?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
}
}
// MARK: URLSessionTaskDelegate methods
extension SCDownloadManager: URLSessionTaskDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if downloadSession == session {
DispatchQueue.main.async {
if let completionHandler = Constants.appDelegate.backgroundTransferCompletionHandler {
Constants.appDelegate.backgroundTransferCompletionHandler = nil
completionHandler()
}
}
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let sourceURL = task.originalRequest?.url else { return }
if session == dataSession {
if error == nil {
} else {
print("got error while doing API on download task")
}
} else {
operations[sourceURL]?.urlSession(session, task: task, didCompleteWithError: error)
operations.removeValue(forKey: sourceURL)
}
}
}
Operation Class :
class SCDownloadOperation: SCAsynchronousOperation {
let downloadRecord: DownloadRecord
var getServerDownloadUrl: ((URLRequest)->Void)?
weak var delegate: SCDownloadOperationDelegate?
init(withDownloadRecord dr: DownloadRecord) {
downloadRecord = dr
super.init()
}
override func cancel() {
if let task = downloadRecord.downloadTask{
task.cancel()
}
super.cancel()
}
override func main() {
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
let configuration = RequestConfiguration(withMethodType: .GET, urlString: downloadRecord.downloadUrl.absoluteString)
if let request = NetworkManager.sharedInstance.urlRequest(forRequestConfiguration: configuration) {
delegate?.getServerDownloadUrl(forRequest: request, ofOperation: self)
}
default: break
}
}
}
// MARK: NSURLSessionDownloadDelegate methods
extension SCDownloadOperation: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
downloadRecord.progress = 1
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
processDownloadFinish(atLocation: location)
default: break
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
downloadRecord.isExecuting = true
downloadRecord.isDownloadProgressAvailable = true
let downloadProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
downloadRecord.progress = downloadProgress
downloadRecord.percentComplete = ((Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))*100)
downloadRecord.downloadProgress = downloadProgress
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
updateWrittenData()
default: break
}
}
}
// MARK: NSURLSessionTaskDelegate methods
extension SCDownloadOperation: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
completeOperation()
if error != nil {
print("\(String(describing: error))")
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
contentDownloadFail()
default: break
}
}
}
}

Resources