URLSession delegates not working after app resume - ios

I've recently been integrating Background Transfer Service into an application so that the user is able to download files in the background.
Everything works as expected. But my delegate methods stops getting called after sending the application into the background and then re-opening the application.
File is actually being downloaded in the background but I am not receiving any call to my delegate methods. So cant show any progress to the users. So it feels like download is got stuck.
I had to remove our app from the app store as it is hurting our app. I need to resubmit the app as soon as possible. But with this problem, it's not possible.
My download manager code:
import Foundation
import Zip
import UserNotifications
////------------------------------------------------------
//// MARK: - Download Progress Struct
////------------------------------------------------------
public struct DownloadProgress {
public let name: String
public let progress: Float
public let completedUnitCount: Float
public let totalUnitCount: Float
}
protocol DownloadDelegate: class {
func downloadProgressUpdate(for progress: DownloadProgress)
func unzipProgressUpdate(for progress: Double)
func onFailure()
}
class DownloadManager : NSObject, URLSessionDownloadDelegate {
//------------------------------------------------------
// MARK: - Downloader Properties
//------------------------------------------------------
static var shared = DownloadManager()
private lazy var session: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).bookDownloader")
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
var delegate: DownloadDelegate?
var previousUrl: URL?
var resumeData: Data?
var task: URLSessionDownloadTask?
// ProgressHandler --> identifier, progress, completedUnitCount, totalUnitCount
typealias ProgressHandler = (String, Float, Float, Float) -> ()
//------------------------------------------------------
// MARK: - Downloader Initializer
//------------------------------------------------------
override private init() {
super.init()
}
func activate() -> URLSession {
// 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 session
}
//------------------------------------------------------
// MARK: - Downloader start download
//------------------------------------------------------
func startDownload(url: URL) {
if let previousUrl = self.previousUrl {
if url == previousUrl {
if let data = resumeData {
let downloadTask = session.downloadTask(withResumeData: data)
downloadTask.resume()
self.task = downloadTask
} else {
let downloadTask = session.downloadTask(with: url)
downloadTask.resume()
self.task = downloadTask
}
} else {
let downloadTask = session.downloadTask(with: url)
downloadTask.resume()
self.task = downloadTask
}
} else {
let downloadTask = session.downloadTask(with: url)
downloadTask.resume()
self.task = downloadTask
}
}
//------------------------------------------------------
// MARK: - Downloader stop download
//------------------------------------------------------
func stopDownload() {
if let task = task {
task.cancel { resumeDataOrNil in
guard let resumeData = resumeDataOrNil else {
// download can't be resumed; remove from UI if necessary
return
}
self.resumeData = resumeData
}
}
}
//------------------------------------------------------
// MARK: - Downloader Progress Calculator
//------------------------------------------------------
private func calculateProgress(session : URLSession, completionHandler : #escaping ProgressHandler) {
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 let name = UserDefaults.standard.string(forKey: UserDefaultKeys.OnBookDownload) {
if name.isEmpty {
return self.session.invalidateAndCancel()
}
completionHandler(name, progress.reduce(0.0, +), countOfBytesReceived.reduce(0.0, +), countOfBytesExpectedToReceive.reduce(0.0, +))
}
}
}
//------------------------------------------------------
// MARK: - Downloader Notifiers
//------------------------------------------------------
func postUnzipProgress(progress: Double) {
if let delegate = self.delegate {
delegate.unzipProgressUpdate(for: progress)
}
// NotificationCenter.default.post(name: .UnzipProgress, object: progress)
}
func postDownloadProgress(progress: DownloadProgress) {
if let delegate = self.delegate {
delegate.downloadProgressUpdate(for: progress)
}
// NotificationCenter.default.post(name: .BookDownloadProgress, 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".localized(), arguments: nil)
content.body = NSString.localizedUserNotificationString(forKey: "Quran Touch app is ready to use".localized(), arguments: nil)
content.sound = UNNotificationSound.default()
content.categoryIdentifier = "com.qurantouch.qurantouch.BookDownloadComplete"
// Deliver the notification in 60 seconds.
let trigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 2.0, repeats: false)
let request = UNNotificationRequest.init(identifier: "BookDownloadCompleted", content: content, trigger: trigger)
// Schedule the notification.
center.add(request)
}
//------------------------------------------------------
// MARK: - Downloader Delegate methods
//------------------------------------------------------
// On Progress Update
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if let name = UserDefaults.standard.string(forKey: UserDefaultKeys.OnBookDownload) {
if name.isEmpty {
return self.session.invalidateAndCancel()
}
} else {
return self.session.invalidateAndCancel()
}
if totalBytesExpectedToWrite > 0 {
calculateProgress(session: session, completionHandler: { (name, progress, completedUnitCount, totalUnitCount) in
let progressInfo = DownloadProgress(name: name, progress: progress, completedUnitCount: completedUnitCount, totalUnitCount: totalUnitCount)
print(progressInfo.progress)
self.postDownloadProgress(progress: progressInfo)
})
}
}
// On Successful Download
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if let name = UserDefaults.standard.string(forKey: UserDefaultKeys.OnBookDownload) {
if name.isEmpty {
return self.session.invalidateAndCancel()
}
let folder = URL.createFolder(folderName: "\(Config.bookFolder)\(name)")
let fileURL = folder!.appendingPathComponent("\(name).zip")
if let url = URL.getFolderUrl(folderName: "\(Config.bookFolder)\(name)") {
do {
try FileManager.default.moveItem(at: location, to: fileURL)
// Download completed. Now time to unzip the file
try Zip.unzipFile((fileURL), destination: url, overwrite: true, password: nil, progress: { (progress) -> () in
if progress == 1 {
App.quranDownloaded = true
UserDefaults.standard.set("selected", forKey: name)
DispatchQueue.main.async {
Reciter().downloadCompleteReciter(success: true).done{_ in}.catch{_ in}
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let backgroundCompletionHandler =
appDelegate.backgroundCompletionHandler else {
return
}
backgroundCompletionHandler()
self.postNotification()
}
// Select the book that is downloaded
// Delete the downlaoded zip file
URL.removeFile(file: fileURL)
}
self.postUnzipProgress(progress: progress)
}, fileOutputHandler: {(outputUrl) -> () in
})
} catch {
print(error)
}
}
} else {
return self.session.invalidateAndCancel()
}
}
// On Dwonload Completed with Failure
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
debugPrint("Task completed: \(task), error: \(error)")
guard let error = error else {
// Handle success case.
return
}
let userInfo = (error as NSError).userInfo
if let resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
self.resumeData = resumeData
}
if let delegate = self.delegate {
if !error.isCancelled {
delegate.onFailure()
}
}
}
// On Dwonload Invalidated with Error
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
guard let error = error else {
// Handle success case.
return
}
if let delegate = self.delegate {
if !error.isCancelled {
delegate.onFailure()
}
}
}
}
// MARK: - URLSessionDelegate
extension DownloadManager: URLSessionDelegate {
// Standard background session handler
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let completionHandler = appDelegate.backgroundCompletionHandler {
completionHandler()
appDelegate.backgroundCompletionHandler = nil
}
}
}
}
And in app delegate:
var backgroundCompletionHandler: (() -> Void)?
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
backgroundCompletionHandler = completionHandler
}

Finally found a workaround for the issue. Once the application did return from background mode, make sure to call resume on all running tasks. This seems to reactivate callbacks to the delegate.
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
DownloadManager.shared.session.getAllTasks(completionHandler: { tasks in
for task in tasks {
task.resume()
}
})
}
For more information on this topic, Follow this link:
https://forums.developer.apple.com/thread/77666

You need to resume all tasks once you come back to active state.
URLSession.shared.getAllTasks { (tasks) in
for task in tasks
{
task.resume()
}
}

Related

How to show Progress in swift - iOS [duplicate]

Is there any way to get progress from dataTaskWithURL in swift while the data is downloading?
NSURLSession.sharedSession().dataTaskWithURL(...)
I need to show progress bar while the data is downloading.
You can simply observe progress property of the URLSessionDataTask object.
Example:
import UIKit
class SomeViewController: UIViewController {
private var observation: NSKeyValueObservation?
deinit {
observation?.invalidate()
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://source.unsplash.com/random/4000x4000")!
let task = URLSession.shared.dataTask(with: url)
observation = task.progress.observe(\.fractionCompleted) { progress, _ in
print("progress: ", progress.fractionCompleted)
}
task.resume()
}
}
Playground example:
import Foundation
import PlaygroundSupport
let page = PlaygroundPage.current
page.needsIndefiniteExecution = true
let url = URL(string: "https://source.unsplash.com/random/4000x4000")!
let task = URLSession.shared.dataTask(with: url) { _, _, _ in
page.finishExecution()
}
// Don't forget to invalidate the observation when you don't need it anymore.
let observation = task.progress.observe(\.fractionCompleted) { progress, _ in
print(progress.fractionCompleted)
}
task.resume()
you can use this code for showing download process with progress bar with its delegate functions.
import UIKit
class ViewController: UIViewController,NSURLSessionDelegate,NSURLSessionDataDelegate{
#IBOutlet weak var progress: UIProgressView!
var buffer:NSMutableData = NSMutableData()
var session:NSURLSession?
var dataTask:NSURLSessionDataTask?
let url = NSURL(string:"http://i.stack.imgur.com/b8zkg.png" )!
var expectedContentLength = 0
override func viewDidLoad() {
super.viewDidLoad()
progress.progress = 0.0
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let manqueue = NSOperationQueue.mainQueue()
session = NSURLSession(configuration: configuration, delegate:self, delegateQueue: manqueue)
dataTask = session?.dataTaskWithRequest(NSURLRequest(URL: url))
dataTask?.resume()
// Do any additional setup after loading the view, typically from a nib.
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
//here you can get full lenth of your content
expectedContentLength = Int(response.expectedContentLength)
println(expectedContentLength)
completionHandler(NSURLSessionResponseDisposition.Allow)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
buffer.appendData(data)
let percentageDownloaded = Float(buffer.length) / Float(expectedContentLength)
progress.progress = percentageDownloaded
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
//use buffer here.Download is done
progress.progress = 1.0 // download 100% complete
}
}
Update for Swift4:
Supports execution of multiple simultaneous operations.
File: DownloadService.swift. Keeps reference to URLSession and tracks executing tasks.
final class DownloadService: NSObject {
private var session: URLSession!
private var downloadTasks = [GenericDownloadTask]()
public static let shared = DownloadService()
private override init() {
super.init()
let configuration = URLSessionConfiguration.default
session = URLSession(configuration: configuration,
delegate: self, delegateQueue: nil)
}
func download(request: URLRequest) -> DownloadTask {
let task = session.dataTask(with: request)
let downloadTask = GenericDownloadTask(task: task)
downloadTasks.append(downloadTask)
return downloadTask
}
}
extension DownloadService: URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse,
completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
guard let task = downloadTasks.first(where: { $0.task == dataTask }) else {
completionHandler(.cancel)
return
}
task.expectedContentLength = response.expectedContentLength
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let task = downloadTasks.first(where: { $0.task == dataTask }) else {
return
}
task.buffer.append(data)
let percentageDownloaded = Double(task.buffer.count) / Double(task.expectedContentLength)
DispatchQueue.main.async {
task.progressHandler?(percentageDownloaded)
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let index = downloadTasks.index(where: { $0.task == task }) else {
return
}
let task = downloadTasks.remove(at: index)
DispatchQueue.main.async {
if let e = error {
task.completionHandler?(.failure(e))
} else {
task.completionHandler?(.success(task.buffer))
}
}
}
}
File: DownloadTask.swift. Lightweight interface just to hide concrete implementation.
protocol DownloadTask {
var completionHandler: ResultType<Data>.Completion? { get set }
var progressHandler: ((Double) -> Void)? { get set }
func resume()
func suspend()
func cancel()
}
File: GenericDownloadTask.swift. Concrete implementation of DownloadTask interface.
class GenericDownloadTask {
var completionHandler: ResultType<Data>.Completion?
var progressHandler: ((Double) -> Void)?
private(set) var task: URLSessionDataTask
var expectedContentLength: Int64 = 0
var buffer = Data()
init(task: URLSessionDataTask) {
self.task = task
}
deinit {
print("Deinit: \(task.originalRequest?.url?.absoluteString ?? "")")
}
}
extension GenericDownloadTask: DownloadTask {
func resume() {
task.resume()
}
func suspend() {
task.suspend()
}
func cancel() {
task.cancel()
}
}
File: ResultType.swift. Reusable type to keep result or error.
public enum ResultType<T> {
public typealias Completion = (ResultType<T>) -> Void
case success(T)
case failure(Swift.Error)
}
Usage: Example how to run two download tasks in parallel (macOS App):
class ViewController: NSViewController {
#IBOutlet fileprivate weak var loadImageButton1: NSButton!
#IBOutlet fileprivate weak var loadProgressIndicator1: NSProgressIndicator!
#IBOutlet fileprivate weak var imageView1: NSImageView!
#IBOutlet fileprivate weak var loadImageButton2: NSButton!
#IBOutlet fileprivate weak var loadProgressIndicator2: NSProgressIndicator!
#IBOutlet fileprivate weak var imageView2: NSImageView!
fileprivate var downloadTask1: DownloadTask?
fileprivate var downloadTask2: DownloadTask?
override func viewDidLoad() {
super.viewDidLoad()
loadImageButton1.target = self
loadImageButton1.action = #selector(startDownload1(_:))
loadImageButton2.target = self
loadImageButton2.action = #selector(startDownload2(_:))
}
}
extension ViewController {
#objc fileprivate func startDownload1(_ button: NSButton) {
let url = URL(string: "http://localhost:8001/?imageID=01&tilestamp=\(Date.timeIntervalSinceReferenceDate)")!
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
downloadTask1 = DownloadService.shared.download(request: request)
downloadTask1?.completionHandler = { [weak self] in
switch $0 {
case .failure(let error):
print(error)
case .success(let data):
print("Number of bytes: \(data.count)")
self?.imageView1.image = NSImage(data: data)
}
self?.downloadTask1 = nil
self?.loadImageButton1.isEnabled = true
}
downloadTask1?.progressHandler = { [weak self] in
print("Task1: \($0)")
self?.loadProgressIndicator1.doubleValue = $0
}
loadImageButton1.isEnabled = false
imageView1.image = nil
loadProgressIndicator1.doubleValue = 0
downloadTask1?.resume()
}
#objc fileprivate func startDownload2(_ button: NSButton) {
let url = URL(string: "http://localhost:8002/?imageID=02&tilestamp=\(Date.timeIntervalSinceReferenceDate)")!
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
downloadTask2 = DownloadService.shared.download(request: request)
downloadTask2?.completionHandler = { [weak self] in
switch $0 {
case .failure(let error):
print(error)
case .success(let data):
print("Number of bytes: \(data.count)")
self?.imageView2.image = NSImage(data: data)
}
self?.downloadTask2 = nil
self?.loadImageButton2.isEnabled = true
}
downloadTask2?.progressHandler = { [weak self] in
print("Task2: \($0)")
self?.loadProgressIndicator2.doubleValue = $0
}
loadImageButton2.isEnabled = false
imageView2.image = nil
loadProgressIndicator2.doubleValue = 0
downloadTask2?.resume()
}
}
Bonus 1. File StartPHPWebServer.command. Example script to run 2 Build-in PHP servers to simulate simultaneous downloads.
#!/bin/bash
AWLScriptDirPath=$(cd "$(dirname "$0")"; pwd)
cd "$AWLScriptDirPath"
php -S localhost:8001 &
php -S localhost:8002 &
ps -afx | grep php
echo "Press ENTER to exit."
read
killall php
Bonus 2. File index.php. Example PHP script to implement slow download.
<?php
$imageID = $_REQUEST["imageID"];
$local_file = "Image-$imageID.jpg";
$download_rate = 20.5; // set the download rate limit (=> 20,5 kb/s)
if (file_exists($local_file) && is_file($local_file)) {
header('Cache-control: private');
header('Content-Type: image/jpeg');
header('Content-Length: '.filesize($local_file));
flush();
$file = fopen($local_file, "r");
while(!feof($file)) {
// send the current file part to the browser
print fread($file, round($download_rate * 1024));
flush(); // flush the content to the browser
usleep(0.25 * 1000000);
}
fclose($file);}
else {
die('Error: The file '.$local_file.' does not exist!');
}
?>
Misc: Contents of directory which simulates 2 PHP servers.
Image-01.jpg
Image-02.jpg
StartPHPWebServer.command
index.php
in class declare
class AudioPlayerViewController: UIViewController, URLSessionDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate{
var defaultSession: URLSession!
var downloadTask: URLSessionDownloadTask!
in download method
defaultSession = Foundation.URLSession(configuration: .default, delegate: self, delegateQueue: nil)
downloadProgress.setProgress(0.0, animated: false)
downloadTask = defaultSession.downloadTask(with: audioUrl)
downloadTask.Resume()
And add following delegates:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
DispatchQueue.main.async {
self.downloadProgressBar.setProgress(Float(totalBytesWritten)/Float(totalBytesExpectedToWrite), animated: true)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
DispatchQueue.main.async {
//DOWNLOAD SUCCESSFUL AND FILE PATH WILL BE IN URL.
}
}
In reference to Dharmesh's work, there have been quite a number of minor changes with URLSessionDelegate and URLSessionDataDelegate delegate methods. Here come the code for Swift 4.2 compatibility.
import UIKit
class ViewController: UIViewController, URLSessionDelegate, URLSessionDataDelegate {
// MARK: - Variables
// MARK: - IBOutlet
#IBOutlet weak var progress: UIProgressView!
// MARK: - IBAction
#IBAction func goTapped(_ sender: UIButton) {
let url = URL(string: "http://www.example.com/file.zip")!
fetchFile(url: url)
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
func fetchFile(url: URL) {
progress.progress = 0.0
let configuration = URLSessionConfiguration.default
let mainQueue = OperationQueue.main
session = URLSession(configuration: configuration, delegate: self, delegateQueue: mainQueue)
dataTask = session?.dataTask(with: URLRequest(url: url))
dataTask?.resume()
}
var buffer: NSMutableData = NSMutableData()
var session: URLSession?
var dataTask: URLSessionDataTask?
var expectedContentLength = 0
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
buffer.append(data)
let percentageDownloaded = Float(buffer.length) / Float(expectedContentLength)
progress.progress = percentageDownloaded
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: (URLSession.ResponseDisposition) -> Void) {
expectedContentLength = Int(response.expectedContentLength)
completionHandler(URLSession.ResponseDisposition.allow)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
progress.progress = 1.0
}
}
for the data been download you need to set the NSURLSessionDownloadDelegate and to implement URLSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)
There is a nice tutorial about it here but is in object-c.

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()
}

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
}
}
}
}

Swift 3 : How to sequential process download files and open ViewController

I want to click a button, then download a number of files sequentially and after complete download, then open a webview to display. But I encounter download files not completed the webview already opened. I saw some approach in days and don't know how to fix the problem. Can anyone help?
Many Thanks.
DownloadManager Class
class DownloadManager: NSObject {
/// Dictionary of operations, keyed by the `taskIdentifier` of the `URLSessionTask`
fileprivate var operations = [Int: DownloadOperation]()
/// Serial NSOperationQueue 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 NSURLSession 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 addDownload(_ 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: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)
if manager.fileExists(atPath: destinationURL.path) {
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: NSURLSessionTaskDelegate methods
extension DownloadOperation: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
completeOperation()
if error != nil {
print("\(error)")
}
}
}
/// 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 `completeOperation()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `completeOperation()` is called.
public class AsynchronousOperation : Operation {
override public var isAsynchronous: Bool { return true }
private let stateLock = NSLock()
private var _executing: Bool = false
override private(set) public var isExecuting: Bool {
get {
return stateLock.withCriticalScope { _executing }
}
set {
willChangeValue(forKey: "isExecuting")
stateLock.withCriticalScope { _executing = newValue }
didChangeValue(forKey: "isExecuting")
}
}
private var _finished: Bool = false
override private(set) public var isFinished: Bool {
get {
return stateLock.withCriticalScope { _finished }
}
set {
willChangeValue(forKey: "isFinished")
stateLock.withCriticalScope { _finished = newValue }
didChangeValue(forKey: "isFinished")
}
}
/// Complete the operation
///
/// This will result in the appropriate KVN of isFinished and isExecuting
public func completeOperation() {
if isExecuting {
isExecuting = false
}
if !isFinished {
isFinished = true
}
}
override public func start() {
if isCancelled {
isFinished = true
return
}
isExecuting = true
main()
}
}
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this sample’s licensing information
Abstract:
An extension to `NSLock` to simplify executing critical code.
From Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/
From https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip
*/
extension NSLock {
/// Perform closure within lock.
///
/// An extension to `NSLock` to simplify executing critical code.
///
/// - parameter block: The closure to be performed.
func withCriticalScope<T>(block: () -> T) -> T {
lock()
let value = block()
unlock()
return value
}
}
Then, I call below code when click download button.
func downloadFiles() {
do {
var localJson: JSON?
let serverJson = self.serverJson
let str = serverJson?.description
let data = str?.data(using: .utf8)
let fileManager = FileManager.default
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! as URL
let oldManifestUrl = documentsUrl.appendingPathComponent("manifest.json")
let oldManifestPath = oldManifestUrl.path
if fileManager.fileExists(atPath: oldManifestPath) {
let jsonData = NSData(contentsOfFile:oldManifestPath)
localJson = JSON(data: jsonData! as Data)
}
var totalCount = 0
let downloadManager = DownloadManager()
for (index, subJson): (String, JSON) in serverJson! {
for (_, subJson): (String, JSON) in subJson {
let filepath = subJson["path"].stringValue
let nUpdated = subJson["updated"].stringValue
if let oUpdated = localJson?[index].array?.filter({ $0["path"].string == filepath}).first?["updated"].stringValue {
if (oUpdated == nUpdated) { continue }
}
var absPath = filepath
let strIdx = absPath.index(absPath.startIndex, offsetBy: 2)
if (absPath.hasPrefix("./"))
{
absPath = absPath.substring(from: strIdx)
}
let sourceUrl = URL(string: self.sourceUrl.appending(absPath))
downloadManager.addDownload(sourceUrl!)
}
}
// Remove temp json file first if exists.
if fileManager.fileExists(atPath: oldManifestPath) {
try? fileManager.removeItem(atPath: oldManifestPath)
}
// Write temp json file to local.
try data?.write(to: oldManifestUrl)
self.defaults.set(hashes, forKey: "LastHash")
let alertController = UIAlertController(title: "Information", message: "Download completed", preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.present(alertController, animated: true, completion: {
self.openWebView()
})
} catch {
print("Write JSON data to file failed, \(error.localizedDescription)")
}
}
What you need is a promise library that offers code execution on certain completion conditions. Currently, you make the request and open the webview straight away. The requests take time to load but your code doesn't tell the device to wait for the requests to finish before opening up the webview. You can use PromiseKit for this purpose. With this library, you can send multiple requests asynchronously and then open the webview when all of the requests have been processed. You will have to explore it in order to understand how it works. Basically, your code structure will look something like this
var promiseArray = [Promise<Void>]()
for WHATEVER CONDITION YOU WANT TO USE FOR THE LOOP {
let promise = YOUR REQUEST GOES HERE
promiseArray.append(promise)
}
_ = when(fulfilled: promiseArray).then {
OPEN WEBVIEW HERE
}

get progress from dataTaskWithURL in swift

Is there any way to get progress from dataTaskWithURL in swift while the data is downloading?
NSURLSession.sharedSession().dataTaskWithURL(...)
I need to show progress bar while the data is downloading.
You can simply observe progress property of the URLSessionDataTask object.
Example:
import UIKit
class SomeViewController: UIViewController {
private var observation: NSKeyValueObservation?
deinit {
observation?.invalidate()
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://source.unsplash.com/random/4000x4000")!
let task = URLSession.shared.dataTask(with: url)
observation = task.progress.observe(\.fractionCompleted) { progress, _ in
print("progress: ", progress.fractionCompleted)
}
task.resume()
}
}
Playground example:
import Foundation
import PlaygroundSupport
let page = PlaygroundPage.current
page.needsIndefiniteExecution = true
let url = URL(string: "https://source.unsplash.com/random/4000x4000")!
let task = URLSession.shared.dataTask(with: url) { _, _, _ in
page.finishExecution()
}
// Don't forget to invalidate the observation when you don't need it anymore.
let observation = task.progress.observe(\.fractionCompleted) { progress, _ in
print(progress.fractionCompleted)
}
task.resume()
you can use this code for showing download process with progress bar with its delegate functions.
import UIKit
class ViewController: UIViewController,NSURLSessionDelegate,NSURLSessionDataDelegate{
#IBOutlet weak var progress: UIProgressView!
var buffer:NSMutableData = NSMutableData()
var session:NSURLSession?
var dataTask:NSURLSessionDataTask?
let url = NSURL(string:"http://i.stack.imgur.com/b8zkg.png" )!
var expectedContentLength = 0
override func viewDidLoad() {
super.viewDidLoad()
progress.progress = 0.0
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let manqueue = NSOperationQueue.mainQueue()
session = NSURLSession(configuration: configuration, delegate:self, delegateQueue: manqueue)
dataTask = session?.dataTaskWithRequest(NSURLRequest(URL: url))
dataTask?.resume()
// Do any additional setup after loading the view, typically from a nib.
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
//here you can get full lenth of your content
expectedContentLength = Int(response.expectedContentLength)
println(expectedContentLength)
completionHandler(NSURLSessionResponseDisposition.Allow)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
buffer.appendData(data)
let percentageDownloaded = Float(buffer.length) / Float(expectedContentLength)
progress.progress = percentageDownloaded
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
//use buffer here.Download is done
progress.progress = 1.0 // download 100% complete
}
}
Update for Swift4:
Supports execution of multiple simultaneous operations.
File: DownloadService.swift. Keeps reference to URLSession and tracks executing tasks.
final class DownloadService: NSObject {
private var session: URLSession!
private var downloadTasks = [GenericDownloadTask]()
public static let shared = DownloadService()
private override init() {
super.init()
let configuration = URLSessionConfiguration.default
session = URLSession(configuration: configuration,
delegate: self, delegateQueue: nil)
}
func download(request: URLRequest) -> DownloadTask {
let task = session.dataTask(with: request)
let downloadTask = GenericDownloadTask(task: task)
downloadTasks.append(downloadTask)
return downloadTask
}
}
extension DownloadService: URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse,
completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
guard let task = downloadTasks.first(where: { $0.task == dataTask }) else {
completionHandler(.cancel)
return
}
task.expectedContentLength = response.expectedContentLength
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let task = downloadTasks.first(where: { $0.task == dataTask }) else {
return
}
task.buffer.append(data)
let percentageDownloaded = Double(task.buffer.count) / Double(task.expectedContentLength)
DispatchQueue.main.async {
task.progressHandler?(percentageDownloaded)
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let index = downloadTasks.index(where: { $0.task == task }) else {
return
}
let task = downloadTasks.remove(at: index)
DispatchQueue.main.async {
if let e = error {
task.completionHandler?(.failure(e))
} else {
task.completionHandler?(.success(task.buffer))
}
}
}
}
File: DownloadTask.swift. Lightweight interface just to hide concrete implementation.
protocol DownloadTask {
var completionHandler: ResultType<Data>.Completion? { get set }
var progressHandler: ((Double) -> Void)? { get set }
func resume()
func suspend()
func cancel()
}
File: GenericDownloadTask.swift. Concrete implementation of DownloadTask interface.
class GenericDownloadTask {
var completionHandler: ResultType<Data>.Completion?
var progressHandler: ((Double) -> Void)?
private(set) var task: URLSessionDataTask
var expectedContentLength: Int64 = 0
var buffer = Data()
init(task: URLSessionDataTask) {
self.task = task
}
deinit {
print("Deinit: \(task.originalRequest?.url?.absoluteString ?? "")")
}
}
extension GenericDownloadTask: DownloadTask {
func resume() {
task.resume()
}
func suspend() {
task.suspend()
}
func cancel() {
task.cancel()
}
}
File: ResultType.swift. Reusable type to keep result or error.
public enum ResultType<T> {
public typealias Completion = (ResultType<T>) -> Void
case success(T)
case failure(Swift.Error)
}
Usage: Example how to run two download tasks in parallel (macOS App):
class ViewController: NSViewController {
#IBOutlet fileprivate weak var loadImageButton1: NSButton!
#IBOutlet fileprivate weak var loadProgressIndicator1: NSProgressIndicator!
#IBOutlet fileprivate weak var imageView1: NSImageView!
#IBOutlet fileprivate weak var loadImageButton2: NSButton!
#IBOutlet fileprivate weak var loadProgressIndicator2: NSProgressIndicator!
#IBOutlet fileprivate weak var imageView2: NSImageView!
fileprivate var downloadTask1: DownloadTask?
fileprivate var downloadTask2: DownloadTask?
override func viewDidLoad() {
super.viewDidLoad()
loadImageButton1.target = self
loadImageButton1.action = #selector(startDownload1(_:))
loadImageButton2.target = self
loadImageButton2.action = #selector(startDownload2(_:))
}
}
extension ViewController {
#objc fileprivate func startDownload1(_ button: NSButton) {
let url = URL(string: "http://localhost:8001/?imageID=01&tilestamp=\(Date.timeIntervalSinceReferenceDate)")!
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
downloadTask1 = DownloadService.shared.download(request: request)
downloadTask1?.completionHandler = { [weak self] in
switch $0 {
case .failure(let error):
print(error)
case .success(let data):
print("Number of bytes: \(data.count)")
self?.imageView1.image = NSImage(data: data)
}
self?.downloadTask1 = nil
self?.loadImageButton1.isEnabled = true
}
downloadTask1?.progressHandler = { [weak self] in
print("Task1: \($0)")
self?.loadProgressIndicator1.doubleValue = $0
}
loadImageButton1.isEnabled = false
imageView1.image = nil
loadProgressIndicator1.doubleValue = 0
downloadTask1?.resume()
}
#objc fileprivate func startDownload2(_ button: NSButton) {
let url = URL(string: "http://localhost:8002/?imageID=02&tilestamp=\(Date.timeIntervalSinceReferenceDate)")!
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
downloadTask2 = DownloadService.shared.download(request: request)
downloadTask2?.completionHandler = { [weak self] in
switch $0 {
case .failure(let error):
print(error)
case .success(let data):
print("Number of bytes: \(data.count)")
self?.imageView2.image = NSImage(data: data)
}
self?.downloadTask2 = nil
self?.loadImageButton2.isEnabled = true
}
downloadTask2?.progressHandler = { [weak self] in
print("Task2: \($0)")
self?.loadProgressIndicator2.doubleValue = $0
}
loadImageButton2.isEnabled = false
imageView2.image = nil
loadProgressIndicator2.doubleValue = 0
downloadTask2?.resume()
}
}
Bonus 1. File StartPHPWebServer.command. Example script to run 2 Build-in PHP servers to simulate simultaneous downloads.
#!/bin/bash
AWLScriptDirPath=$(cd "$(dirname "$0")"; pwd)
cd "$AWLScriptDirPath"
php -S localhost:8001 &
php -S localhost:8002 &
ps -afx | grep php
echo "Press ENTER to exit."
read
killall php
Bonus 2. File index.php. Example PHP script to implement slow download.
<?php
$imageID = $_REQUEST["imageID"];
$local_file = "Image-$imageID.jpg";
$download_rate = 20.5; // set the download rate limit (=> 20,5 kb/s)
if (file_exists($local_file) && is_file($local_file)) {
header('Cache-control: private');
header('Content-Type: image/jpeg');
header('Content-Length: '.filesize($local_file));
flush();
$file = fopen($local_file, "r");
while(!feof($file)) {
// send the current file part to the browser
print fread($file, round($download_rate * 1024));
flush(); // flush the content to the browser
usleep(0.25 * 1000000);
}
fclose($file);}
else {
die('Error: The file '.$local_file.' does not exist!');
}
?>
Misc: Contents of directory which simulates 2 PHP servers.
Image-01.jpg
Image-02.jpg
StartPHPWebServer.command
index.php
in class declare
class AudioPlayerViewController: UIViewController, URLSessionDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate{
var defaultSession: URLSession!
var downloadTask: URLSessionDownloadTask!
in download method
defaultSession = Foundation.URLSession(configuration: .default, delegate: self, delegateQueue: nil)
downloadProgress.setProgress(0.0, animated: false)
downloadTask = defaultSession.downloadTask(with: audioUrl)
downloadTask.Resume()
And add following delegates:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
DispatchQueue.main.async {
self.downloadProgressBar.setProgress(Float(totalBytesWritten)/Float(totalBytesExpectedToWrite), animated: true)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
DispatchQueue.main.async {
//DOWNLOAD SUCCESSFUL AND FILE PATH WILL BE IN URL.
}
}
In reference to Dharmesh's work, there have been quite a number of minor changes with URLSessionDelegate and URLSessionDataDelegate delegate methods. Here come the code for Swift 4.2 compatibility.
import UIKit
class ViewController: UIViewController, URLSessionDelegate, URLSessionDataDelegate {
// MARK: - Variables
// MARK: - IBOutlet
#IBOutlet weak var progress: UIProgressView!
// MARK: - IBAction
#IBAction func goTapped(_ sender: UIButton) {
let url = URL(string: "http://www.example.com/file.zip")!
fetchFile(url: url)
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
func fetchFile(url: URL) {
progress.progress = 0.0
let configuration = URLSessionConfiguration.default
let mainQueue = OperationQueue.main
session = URLSession(configuration: configuration, delegate: self, delegateQueue: mainQueue)
dataTask = session?.dataTask(with: URLRequest(url: url))
dataTask?.resume()
}
var buffer: NSMutableData = NSMutableData()
var session: URLSession?
var dataTask: URLSessionDataTask?
var expectedContentLength = 0
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
buffer.append(data)
let percentageDownloaded = Float(buffer.length) / Float(expectedContentLength)
progress.progress = percentageDownloaded
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: (URLSession.ResponseDisposition) -> Void) {
expectedContentLength = Int(response.expectedContentLength)
completionHandler(URLSession.ResponseDisposition.allow)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
progress.progress = 1.0
}
}
for the data been download you need to set the NSURLSessionDownloadDelegate and to implement URLSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)
There is a nice tutorial about it here but is in object-c.

Resources