Migrating code from NSURLConnection to NSURLSession [duplicate] - ios

In the following code, the file downloads just fine. However none of the delegate methods seem to be called as I receive no output whatsoever. the progressView is not updated either. Any idea why?
import Foundation
import UIKit
class Podcast: PFQueryTableViewController, UINavigationControllerDelegate, MWFeedParserDelegate, UITableViewDataSource, NSURLSessionDelegate, NSURLSessionDownloadDelegate {
func downloadEpisodeWithFeedItem(episodeURL: NSURL) {
var request: NSURLRequest = NSURLRequest(URL: episodeURL)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
var downloadTask = session.downloadTaskWithURL(episodeURL, completionHandler: { (url, response, error) -> Void in
println("task completed")
if (error != nil) {
println(error.localizedDescription)
} else {
println("no error")
println(response)
}
})
downloadTask.resume()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
println("didResumeAtOffset")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
var downloadProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
println(Float(downloadProgress))
println("sup")
epCell.progressView.progress = Float(downloadProgress)
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
println(location)
}
}

From my testing, you have to choose whether you want to use a delegate or a completion handler - if you specify both, only the completion handler gets called. This code gave me running progress updates and the didFinishDownloadingToURL event:
func downloadEpisodeWithFeedItem(episodeURL: NSURL) {
let request: NSURLRequest = NSURLRequest(URL: episodeURL)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
let downloadTask = session.downloadTaskWithURL(episodeURL)
downloadTask.resume()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
println("didResumeAtOffset: \(fileOffset)")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
var downloadProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
println("downloadProgress: \(downloadProgress)")
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
println("didFinishDownloadingToURL: \(location)")
println(downloadTask)
}
From the NSURLSession documentation, here's the relevant section:
Like most networking APIs, the NSURLSession API is highly asynchronous. It returns data in one of two ways, depending on the methods you call:
To a completion handler block that returns data to your app when a transfer finishes successfully or with an error.
By calling methods on your custom delegate as the data is received.
By calling methods on your custom delegate when download to a file is complete.
So by design it returns data to either a completion handler block or a delegate. But as evinced here, not both.

Interestingly, Apple specifically explains this behavior in their NSURLSessionDataDelegate (but neither in the base delegate NSURLSessionTaskDelegate nor in NSURLSessionDownloadDelegate)
NOTE
An NSURLSession object need not have a delegate. If no delegate is assigned, when you create tasks in that session, you must provide a completion handler block to obtain the data.
Completion handler block are primarily intended as an alternative to using a custom delegate. If you create a task using a method that takes a completion handler block, the delegate methods for response and data delivery are not called.

Swift 3
class ViewController: UIViewController {
var urlLink: URL!
var defaultSession: URLSession!
var downloadTask: URLSessionDownloadTask!
}
// MARK: Button Pressed
#IBAction func btnDownloadPressed(_ sender: UIButton) {
let urlLink1 = URL.init(string: "https://github.com/VivekVithlani/QRCodeReader/archive/master.zip")
startDownloading(url: urlLink!)
}
#IBAction func btnResumePressed(_ sender: UIButton) {
downloadTask.resume()
}
#IBAction func btnStopPressed(_ sender: UIButton) {
downloadTask.cancel()
}
#IBAction func btnPausePressed(_ sender: UIButton) {
downloadTask.suspend()
}
func startDownloading (url:URL) {
let backgroundSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundSession")
defaultSession = Foundation.URLSession(configuration: backgroundSessionConfiguration, delegate: self, delegateQueue: OperationQueue.main)
downloadProgress.setProgress(0.0, animated: false)
downloadTask = defaultSession.downloadTask(with: urlLink)
downloadTask.resume()
}
// MARK:- URLSessionDownloadDelegate
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("File download succesfully")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
downloadProgress.setProgress(Float(totalBytesWritten)/Float(totalBytesExpectedToWrite), animated: true)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
downloadTask = nil
downloadProgress.setProgress(0.0, animated: true)
if (error != nil) {
print("didCompleteWithError \(error?.localizedDescription)")
}
else {
print("The task finished successfully")
}
}

Related

Swift background download task got suspended when I application goes into background

I am trying to create a download task that will continue to download a video even when the application is in the background.
So I followed the apple documentation to create a url session as background.
When I started the download process by tapped a button, I will print the progress of the download.
However, the application stops printing the progress when the application goes into background.
I wonder what did I miss, or misunderstand.
class ViewController: UIViewController {
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "MySession")
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue:
OperationQueue())
}()
var downloadUrl = "https://archive.rthk.hk/mp4/tv/2021/THKCCT2021M06000036.mp4"
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func downloadButtonTapped(_ sender: UIButton) {
print("DownloadButton Tapped")
let url = URL(string: downloadUrl)
guard let url = url else {
print("Invalid URL")
return
}
let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()
}
}
extension ViewController: URLSessionDownloadDelegate, URLSessionDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
print("Downloaded, location: \(location.path)")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
print("Progress: \(progress)")
}
}
You need to configure the background tasks to make that work properly.
For more details to configure background tasks please check this official apple developer documentation
https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background

How to use URLSession delegate with custom class

I'm used to making a custom class for URLSession using singleton. So I'm using it with URLSession delegate first time and I'm getting confused. Because delegate method is not called!
Can I know any solution? Did I miss something?
And I just want to check whether making a custom class for URLSession is a good thing?
Here is my code. This is my custom API for URLSession.
import UIKit
class CustomNetworkAPI {
static let shared = CustomNetworkAPI()
var session: URLSession?
private var sessionDataTask: URLSessionDataTask?
private var sessionDownloadTask: URLSessionDownloadTask?
var cache: URLCache?
private init() {}
func downloadTaskForImage(_ url: URL, _ completionHandler: #escaping (Result<URL, Error>) -> ()) {
// print(session.delegate)
sessionDownloadTask = session?.downloadTask(with: url, completionHandler: { (url, response, error) in
if let error = error {
completionHandler(.failure(error))
return
}
guard let url = url else {
completionHandler(.failure(URLRequestError.dataError))
return
}
completionHandler(.success(url))
})
sessionDownloadTask?.resume()
}
}
And this is sample code(not whole thing) of VC using the api and delegate.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let api = CustomNetworkAPI.shared
let config = URLSessionConfiguration.default
api.session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
guard let url = URL(string: "...") else { return }
api.downloadTaskForImage(url) { (result) in
switch result {
case .success(let url):
print(url)
case .failure(let error):
print(error)
}
}
}
}
extension ViewController: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("down load complete")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
print("ing")
}
}
I was missing something, which is this quote below that I found in apple docs.
Your URLSession object doesn’t need to have a delegate. If no delegate is assigned, a system-provided delegate is used, and you must provide a completion callback to obtain the data.
So I tried to remove the completion handler block, and the delegate method was called.

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.

Swift cancellation of NSOperationQueue does not work

I'm trying to cancel NSOperationQueue process using method cancelAllOperations() using swift but it does not work.
I am downloading a file from url when i cancel the download it doesn't work.
here is the code i am using:
var s = NSOperationQueue.mainQueue()
lazy var session : NSURLSession = {
let config = NSURLSessionConfiguration.ephemeralSessionConfiguration()
config.allowsCellularAccess = false
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: self.s)
return session
}
override func viewDidLoad() {
progressBar.setProgress(0.0, animated: true) //set progressBar to 0 at start
}
#IBAction func cancel(sender: AnyObject) {
s.cancelAllOperations()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten writ: Int64, totalBytesExpectedToWrite exp: Int64) {
print("downloaded \(100*writ/exp)")
dispatch_async(dispatch_get_main_queue(), {
self.counter = Float(100*writ/exp)
return
})
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
// unused in this example
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
print("completed: error: \(error)")
if(error != nil){
}
}
// this is the only required NSURLSessionDownloadDelegate method
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
let path = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = NSFileManager()
let destinationURLForFile = NSURL(fileURLWithPath: documentDirectoryPath.stringByAppendingString("Music.mp3"))
if fileManager.fileExistsAtPath(destinationURLForFile.path!){
print("File Already Exists")
}
else{
do {
try fileManager.moveItemAtURL(location, toURL: destinationURLForFile)
print("File doesn't exists")
}catch{
print("An error occurred while moving file to destination url")
}
}
}

How to Cancel download in Queue using Swift

I have an app where user can download multiple files, sequentially. I have followed Mr. Rob's solution for the sequential downloads. However, I have problem when I try to cancel the download.
There are two situation when I try to cancel the download.
I want to cancel the current downloading file. When I cancel that file, the download can proceed to the next file in the queue
I want to cancel the file that are currently in the queue. The queue has cancelAll() method which will cancel all file in the queue.
Here is the codes
DownloadManager.swift
class DownloadManager: NSObject, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate
{
/// Dictionary of operations, keyed by the `taskIdentifier` of the `NSURLSessionTask`
internal var delegate : DownloadVC!
private var operations = [Int: DownloadOperation]()
/// Serial NSOperationQueue for downloads
let queue: NSOperationQueue = {
let _queue = NSOperationQueue()
_queue.name = "download"
_queue.maxConcurrentOperationCount = 1
return _queue
}()
/// Delegate-based NSURLSession for DownloadManager
lazy var session: NSURLSession = {
let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
return NSURLSession(configuration: sessionConfiguration, 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
func addDownload(URL: NSURL) -> DownloadOperation
{
print("url in download manager: \(URL)")
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()
}
// func cancelOne()
// {
// operations[identifier]?.cancel()
// print("identifier : \(identifier)")
// //queue.operations[identifier].cancel()
// // cancelAll()
// }
// MARK: NSURLSessionDownloadDelegate methods
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didFinishDownloadingToURL location: NSURL)
{
print("downloadTask.taskIdentifier \(downloadTask.taskIdentifier)")
operations[downloadTask.taskIdentifier]?.delegate = delegate
operations[downloadTask.taskIdentifier]?.URLSession(session, downloadTask: downloadTask, didFinishDownloadingToURL: location)
}
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
{
operations[downloadTask.taskIdentifier]?.delegate = delegate
operations[downloadTask.taskIdentifier]?.URLSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
// MARK: NSURLSessionTaskDelegate methods
func URLSession(session: NSURLSession,
task: NSURLSessionTask,
didCompleteWithError error: NSError?)
{
let key = task.taskIdentifier
operations[key]?.URLSession(session, task: task, didCompleteWithError: error)
operations.removeValueForKey(key)
}
}
DownloadOperation.swift
class DownloadOperation : AsynchronousOperation
{
let task: NSURLSessionTask
var percentageWritten:Float = 0.0
var delegate : DataDelegate!
init(session: NSURLSession, URL: NSURL)
{
task = session.downloadTaskWithURL(URL)
super.init()
}
override func cancel() {
task.cancel()
super.cancel()
}
override func main() {
task.resume()
}
// MARK: NSURLSessionDownloadDelegate methods
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten write: Int64,
totalBytesExpectedToWrite expect: Int64)
{
//download bar progress
percentageWritten = Float(write) / Float(expect)
//progressBar.progress = percentageWritten
let pW = Int(percentageWritten*100)
delegate.didWriteData(pW)
}
// using cocoa Security for encryption and decryption
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didFinishDownloadingToURL location: NSURL)
{
let documentsDirectoryURL = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first as NSURL?
print("Finished downloading!")
print(documentsDirectoryURL)
let downloadLocation = "\(location)".stringByReplacingOccurrencesOfString("file://", withString: "")
let fileData = NSFileManager().contentsAtPath(downloadLocation)
delegate.didFinishDownloadingToUrl(fileData!)
}
// MARK: NSURLSessionTaskDelegate methods
func URLSession(session: NSURLSession,
task: NSURLSessionTask,
didCompleteWithError error: NSError?)
{
completeOperation()
if error != nil {
print(error)
}
}
}
AsynchronousOperation.swift
class AsynchronousOperation : NSOperation
{
override var asynchronous: Bool { return true }
private var _executing: Bool = false
override var executing: Bool
{
get
{
return _executing
}
set {
if (_executing != newValue)
{
self.willChangeValueForKey("isExecuting")
_executing = newValue
self.didChangeValueForKey("isExecuting")
}
}
}
private var _finished: Bool = false
override var finished: Bool
{
get
{
return _finished
}
set
{
if (_finished != newValue)
{
self.willChangeValueForKey("isFinished")
_finished = newValue
self.didChangeValueForKey("isFinished")
}
}
}
func completeOperation()
{
if executing {
executing = false
finished = true
}
}
override func start()
{
if (cancelled) {
finished = true
executing = false
return
}
executing = true
main()
}
}
DownloadViewController.swift
This is where I want to cancel the download.
if cell.downloadLabel.currentTitle == "CANCEL"
{
print("cancel button was pressed")
downloadManager.cancelAll()
// I want to put the codes here
let defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
let key = cell.stringId
print("key : \(key)")
defaults.setBool(false, forKey: key)
defaults.synchronize()
cell.accessoryType = UITableViewCellAccessoryType.None
cell.downloadLabel.backgroundColor = UIColor.redColor()
cell.downloadLabel.setTitle("DOWNLOAD", forState: .Normal)
}
For situation 1, I have tried to use do
queue.operations[identifier].cancel()
but it crash saying
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
For situation 2, I have tried to put all downloaded urls in an array, add into queue, once the cancel button is clicked, it will cancelAll() queue, remove the cancel one from queue and re-insert the remaining urls back into the array. However, it won't work
Can anybody help me to satisfy both situation?
You need to keep a reference to the NSOperation instance. Then you can call cancel on it.

Resources