Swift 3: Handle Errors in URLSession Delegates - ios

I need to know how to catch errors (interruptions, primarily) in a URLsession with delegates.
I have the following Swift function within a custom class, which downloads a small file to test download speeds:
func testSpeed() {
Globals.shared.dlStartTime = Date()
Globals.shared.DownComplete = false
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
let task = session.downloadTask(with: url!)
if Globals.shared.currentSSID == "" {
Globals.shared.bandwidth = 0
Globals.shared.DownComplete = true
session.invalidateAndCancel()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ProcessFinished"), object: nil, userInfo: nil)
} else {
print("Running Task")
task.resume()
}
}
This class uses URLSessionDelegate and URLSessionDownloadDelegate. Here are the current delegates it calls:
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
Globals.shared.dlFileSize = (Double(totalBytesExpectedToWrite) * 8) / 1000
let progress = (Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100.0
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ProcessUpdating"), object: nil, userInfo: ["progress" : progress])
}
^ That one monitors download progress and uses NotificationCenter to send the progress back to the view controller.
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("Done")
if Globals.shared.DownComplete == false {
let elapsed = Double( Date().timeIntervalSince(Globals.shared.dlStartTime))
Globals.shared.bandwidth = Int(Globals.shared.dlFileSize / elapsed)
Globals.shared.DownComplete = true
Globals.shared.dataUse! += (Globals.shared.dlFileSize! / 8000)
}
session.invalidateAndCancel()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ProcessFinished"), object: nil, userInfo: nil)
}
^ That one just calculates the speed once the download is finished, and sends the result to a global variable in another class. Unimportant.
As of now, when I test my application, interrupting the download just hangs the app, because it keeps waiting for the processFinished NC call, which obviously never comes.
Is there another delegate I should add to catch that interruption, or am I missing something more obvious?

You can use urlSession:task:didCompleteWithError delegate method

Related

AVAssetDownloadTask method 'didFinishDownloadingTo' is not called in background mode with low disk space

I create a background session like this:
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: backgroundSessionId)
backgroundConfiguration.isDiscretionary = false
backgroundConfiguration.sessionSendsLaunchEvents = true backgroundConfiguration.shouldUseExtendedBackgroundIdleMode = true
privateQueue = OperationQueue()
privateQueue.maxConcurrentOperationCount = 1
assetDownloadSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: privateQueue)
Also create and run a task:
let task = assetDownloadSession.makeAssetDownloadTask(asset: urlAsset, assetTitle: title, assetArtworkData: nil, options: nil)
task.resume()
But if I have less than 500MB of disk space on my real device the application is restarted and the following method is called:
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
But the next method is not called:
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL)
The file is not deleted from the device, how can I find out where it is locally and how to delete it? The system does not do this automatically.
Checked many times on real iphone 6s, iOS 14.0.1 with logs and Mac OS console application.
If the memory is more than 500 MB, then everything works correctly and the method didCompleteWithError is called
You can use AVAggregateAssetDownloadTask to get a location of your downloading media in URLSession:aggregateAssetDownloadTask:willDownloadToURL:e.g.:
var assetDownloadURLSession: AVAssetDownloadURLSession!
var task = AVAggregateAssetDownloadTask?
var downloadURL: URL?
func download(asset: AVURLAsset)
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "AAPL-Identifier")
assetDownloadURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
task = assetDownloadURLSession.aggregateAssetDownloadTask(with: asset, ...)
task?.resume()
...
}
func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, willDownloadTo location: URL) {
downloadURL = location
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let url = downloadURL {
do {
try FileManager.default.removeItem(at: url)
}
catch {
print(error)
}
}
}
How to work with aggregate tasks you can look at Apple's sample project: https://developer.apple.com/documentation/avfoundation/media_playback_and_selection/using_avfoundation_to_play_and_persist_http_live_streams

AVAssetDownloadTask doesn't start on IOS 10.3 device

I'm actually implementing a download functionality in an application.
I'm facing a very weird bug with AVAssetDownloadTask.
Indeed at the beginning of the day the implementation for downloading a asset was working.
When I called the resume function on my AssetDownloadTask, newly created, the download started instantly and the AVAssetDownloadDelegate function
func urlSession(
_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didLoad timeRange: CMTimeRange,
totalTimeRangesLoaded loadedTimeRanges: [NSValue],
timeRangeExpectedToLoad: CMTimeRange
)
was called and showed progress for the active downloading task.
But at one point in the day the process stopped working. Now when I try to start/resume a AssetDownloadTask nothing happens.
No AVAssetDownloadDelegate function is called for the progression of the download. It is like nothing is happening. I don't even get an error.
The weird thing is if I cancelled this AssetDownloadTask the
func urlSession(
_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didFinishDownloadingTo location: URL
)
and
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?
)
are correctly called showing that the process seems to be on hold somehow.
I don't seem to have any of this problems on devices that are running on IOS 11.
I was wondering if any of you had an idea that could explain why my AssetDownloadTask doesn't start/resume on IOS 10.3 devices.
You will find below a few parts of my download manager.
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
// Create the AVAssetDownloadURLSession using the configuration.
assetDownloadURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
private var activeDownloadsMap = [AVAssetDownloadTask: XXXXXXX]()
func downloadAsset(for asset: XXXXX) {
// Get the default media selections for the asset's media selection groups.
let preferredMediaSelection = asset.geturlAsset().preferredMediaSelection
if #available(iOS 10.0, *) {
guard let task = assetDownloadURLSession.makeAssetDownloadTask(asset: asset.geturlAsset(),
assetTitle: asset.title,
assetArtworkData: nil,
options: nil) else { return }
// To better track the AVAssetDownloadTask we set the taskDescription to something unique for our sample.
task.taskDescription = asset.title
activeDownloadsMap[task] = asset
task.resume()
} else {
return
}
}
extension DownloadManager: AVAssetDownloadDelegate {
public func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
// This delegate callback should be used to provide download progress for your AVAssetDownloadTask.
guard let asset = activeDownloadsMap[assetDownloadTask] else { return }
var percentComplete = 0.0
for value in loadedTimeRanges {
let loadedTimeRange: CMTimeRange = value.timeRangeValue
percentComplete +=
CMTimeGetSeconds(loadedTimeRange.duration) / CMTimeGetSeconds(timeRangeExpectedToLoad.duration)
}
debugPrint("DOWNLOAD: Progress \( assetDownloadTask) \(percentComplete)")
}
}
Thank you in advance for any help you could give me to try and figure this out.
Cant figure out if it is coming from the device / code / OS version
Best regards,
Martin

Error while implementing AVAssetDownloadURLSession to download HLS stream

I'm trying to implement an offline mode to a streaming application.
The goal is to be able to download an HLS stream on the device of the user to make it possible to watch the stream even while the user is offline.
I have recently stumble on this tutorial.
It seems to answer the exact requirements of what I was trying to implement but I'm facing a problem while trying to make it work.
I've created a little DownloadManager to apply the logic of the tutorial.
Here is my singleton class:
import AVFoundation
class DownloadManager:NSObject {
static var shared = DownloadManager()
private var config: URLSessionConfiguration!
private var downloadSession: AVAssetDownloadURLSession!
override private init() {
super.init()
config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
downloadSession = AVAssetDownloadURLSession(configuration: config, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
}
func setupAssetDownload(_ url: URL) {
let options = [AVURLAssetAllowsCellularAccessKey: false]
let asset = AVURLAsset(url: url, options: options)
// Create new AVAssetDownloadTask for the desired asset
let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
assetTitle: "Test Download",
assetArtworkData: nil,
options: nil)
// Start task and begin download
downloadTask?.resume()
}
func restorePendingDownloads() {
// Grab all the pending tasks associated with the downloadSession
downloadSession.getAllTasks { tasksArray in
// For each task, restore the state in the app
for task in tasksArray {
guard let downloadTask = task as? AVAssetDownloadTask else { break }
// Restore asset, progress indicators, state, etc...
let asset = downloadTask.urlAsset
downloadTask.resume()
}
}
}
func playOfflineAsset() -> AVURLAsset? {
guard let assetPath = UserDefaults.standard.value(forKey: "assetPath") as? String else {
// Present Error: No offline version of this asset available
return nil
}
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
let asset = AVURLAsset(url: assetURL)
if let cache = asset.assetCache, cache.isPlayableOffline {
return asset
// Set up player item and player and begin playback
} else {
return nil
// Present Error: No playable version of this asset exists offline
}
}
func getPath() -> String {
return UserDefaults.standard.value(forKey: "assetPath") as? String ?? ""
}
func deleteOfflineAsset() {
do {
let userDefaults = UserDefaults.standard
if let assetPath = userDefaults.value(forKey: "assetPath") as? String {
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
try FileManager.default.removeItem(at: assetURL)
userDefaults.removeObject(forKey: "assetPath")
}
} catch {
print("An error occured deleting offline asset: \(error)")
}
}
}
extension DownloadManager: AVAssetDownloadDelegate {
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
var percentComplete = 0.0
// Iterate through the loaded time ranges
for value in loadedTimeRanges {
// Unwrap the CMTimeRange from the NSValue
let loadedTimeRange = value.timeRangeValue
// Calculate the percentage of the total expected asset duration
percentComplete += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds
}
percentComplete *= 100
debugPrint("Progress \( assetDownloadTask) \(percentComplete)")
let params = ["percent": percentComplete]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "completion"), object: nil, userInfo: params)
// Update UI state: post notification, update KVO state, invoke callback, etc.
}
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
// Do not move the asset from the download location
UserDefaults.standard.set(location.relativePath, forKey: "assetPath")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
debugPrint("Download finished: \(location)")
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
debugPrint("Task completed: \(task), error: \(String(describing: error))")
guard error == nil else { return }
guard let task = task as? AVAssetDownloadTask else { return }
print("DOWNLOAD: FINISHED")
}
}
My problem comes when I try to call my setupAssetDownload function.
Everytime time I try to resume a downloadTask I get an error message in the urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) delegate function.
The log of the message is:
Task completed: <__NSCFBackgroundAVAssetDownloadTask:
0x7ff57fc024a0>{ taskIdentifier: 1 }, error: Optional(Error
Domain=AVFoundationErrorDomain Code=-11800 \"The operation could not
be completed\" UserInfo={NSLocalizedFailureReason=An unknown error
occurred (-12780), NSLocalizedDescription=The operation could not be
completed})
To give you all the relevant information the URL I past to my setupAssetDownload function is of type
URL(string: "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8")!
I been looking for a cause and solution for this error but I don't seem to be able to find one for the time being.
I would be very grateful for any tips or any clues on how resolve this issue or any indication of errors in my singleton implementation that could explain this behaviour.
Thank you in advance.
Martin
EDIT:
It seems that this bug occurs on a simulator. I launch my app on a real device and the download started without any problem. Hope this helps. Still don't understand why I cannot try this behaviour on a simulator.

How to handle background url session lost internet connection?

I have background downloading zip file:
if let url = NSURL(string: urlstring)
{
let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.downloadTaskWithURL(url)
session.sessionDescription = filepath
if let sessionId = session.configuration.identifier
{
print("start zip session: " + sessionId)
}
task.resume()
}
}
it works cool if you have internet connection but if you lose it during downloading app just wait and URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) will not be called never.
How it can be handle?
Something like time for response from server
The question is old, but still unanswered, so here is one workaround.
Some clarifications first
In general there is a property of URLSessionConfiguration called waitsForConnectivity which can be set to false where URLSessionTask will then directly fail on connection lost. However if true then URLSessionTaskDelegate will receive a callback on the urlSession(_:taskIsWaitingForConnectivity:) method.
However
Background tasks such as DownloadTask always wait for connectivity and ignore waitsForConnectivity property of URLSessionConfiguration. They also DON'T trigger urlSession(_:taskIsWaitingForConnectivity:) callback, so there is no official way to listen for connectivity drop-out on download task.
Workaround
If you listening for the download progress you'll notice that a call to the method is done few times in a second. Therefore we can conclude that if the progress callback is not called for more than 5 seconds, then there could be a connectivity drop issue. So the workaround is to make additional property to the URLSessionDownloadDelegate delegate and store the last update of the progress. Then have interval function to periodically check whether this property were not updated soon.
Something like:
class Downloader: NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate {
var lastUpdate: Date;
var downloadTask: URLSessionDownloadTask?;
public var session : URLSession {
get {
let config = URLSessionConfiguration.background(
withIdentifier: "\(Bundle.main.bundleIdentifier!).downloader");
config.isDiscretionary = true;
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue());
}
}
override init() {
self.lastUpdate = Date();
super.init();
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// Handle download completition
// ...
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten writ: Int64,
totalBytesExpectedToWrite exp: Int64)
{
let progress = 100 * writ / exp;
// Do something with the progress
// ...
self.lastUpdate = Date();
}
}
var downloader = Downloader();
let url = "http://...";
var request = URLRequest(url: URL(string: url)!);
currentWorker.downloadTask = downloader.session.downloadTask(with: request);
currentWorker.downloadTask!.resume();
// Schedule timer for every 5 secs
var timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "CheckInternetDrop", userInfo: nil, repeats: true);
func CheckInternetDrop(){
let interval = Date().timeIntervalSinceDate(downloader.lastUpdate);
if (interval > 5) {
print("connection dropped");
}
}
Very simple example...
According to apple docs on pausing and resuming downloads :
Also, downloads that use a background configuration will handle resumption automatically, so manual resuming is only needed for non-background downloads.
Besides, the reason waitsForConnectivity is being ignored is stated in the downloading files in the background :
If your app is in the background, the system may suspend your app while the download is performed in another process.
This is why even if you were to build a timer in a similar fashion to Dimitar Atanasov it will not work once the app goes in the background since it will be suspended.
I've tested the background downloading myself with network failures and the system resumes without fail, hence no extra code is required.
You can set timeouts for your requests:
config.timeoutIntervalForRequest = <desired value>
config.timeoutIntervalForResource = <desired value>
Documentation:
timeoutIntervalForRequest
timeoutIntervalForResource

Updating UI Dispatch_Async background download Swift

Im working on a folders/files application where users are able to download files to local disk. Whenever a user is downloading a file, I want to show a download bar that displays progress.
to do so, I've created a protocol that allows my download class and my view controller to communicate:
protocol:
protocol DownloadResponder : class {
func downloadFinished()
func downloadProgress(current:Int64, total:Int64)
}
download class:
class fileDownloader: NSObject, NSURLSessionDelegate, NSURLSessionDownloadDelegate {
//responder
var responder : MyAwesomeDownloadResponder?
init(responder : MyAwesomeDownloadResponder) {
self.responder = responder
}
...
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
println("downloaded \(100*totalBytesWritten/totalBytesExpectedToWrite)")
responder?.downloadProgress(totalBytesWritten, total: totalBytesExpectedToWrite)
}
...
}
and then in my view controller I have my download button which trigger the downloadProgress function:
func downloadProgress(current:Int64, total:Int64) {
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some task
var currentProgress = 100 * current / total
dispatch_async(dispatch_get_main_queue()) {
// update some UI
self.downloadLbl.text = "Downloaded \(currentProgress)%"
//set progress bar
self.progressBar.setProgress(Float(currentProgress), animated: true)
}
}
}
While printing information in the console works all the time, updating the UI was not really stable. To fix this I used the dispatch_async method that push the UI change on the main thread. However, while it always work on the first time, poping back to the previous view controller and coming back again, executing the download once more does not trigger the UI updates. The progress bar progressBar.setProgress does nothing and my label downloadLbl.text does not update itself.
Does anyone have an idea about the way to solve this?
If my question lacks information, please let me know and I'll try to add up to the existing information. Thanks!
As I didn't receive / find any solution to my problem I went back to an higher level and changed the way to communicate between my classes to handle ui changes based on background download thread progression.
Instead of using protocols, I went for Notifications and it solved my problem.
Inside the download class:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
println("downloaded \(100*totalBytesWritten/totalBytesExpectedToWrite)")
//NOTIFICATION
// notify download progress!
var fileInfo = [NSObject:AnyObject]()
fileInfo["fileId"] = fileDownloader.storageInfo[downloadTask.taskIdentifier]!["id"] as! Int!
fileInfo["fileCurrent"] = Float(totalBytesWritten)
fileInfo["fileTotal"] = Float(totalBytesExpectedToWrite)
let defaultCenter = NSNotificationCenter.defaultCenter()
defaultCenter.postNotificationName("DownloadProgressNotification",
object: nil,
userInfo: fileInfo)
}
inside the view controller:
override func viewDidLoad() {
super.viewDidLoad()
// ready for receiving notification
let defaultCenter = NSNotificationCenter.defaultCenter()
defaultCenter.addObserver(self,
selector: "handleCompleteDownload:",
name: "DownloadProgressNotification",
object: nil)
}
func handleCompleteDownload(notification: NSNotification) {
let tmp : [NSObject : AnyObject] = notification.userInfo!
// if notification received, change label value
var id = tmp["fileId"] as! Int!
var current = tmp["fileCurrent"] as! Float!
var total = tmp["fileTotal"] as! Float!
var floatCounter = 100 * current / total
var progressCounter = String(format: "%.f", floatCounter)
if(id == self.fileId){
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some task
dispatch_async(dispatch_get_main_queue()) {
// update some UI
self.downloadLbl.text = "Downloaded \(progressCounter)%"
self.progressBar.setProgress((progressCounter as NSString).floatValue, animated: true)
}
}
}
}
hope that will help!

Resources