How to handle background url session lost internet connection? - ios

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

Related

how can i track download progress after app is did become active from background

i have a problem described in title. you may see source code in my repository (https://github.com/Hudayberdyyev/custom_download_manager) . i will try to briefly explain the problem. I am trying to write a download manager based on this repo (https://github.com/r-plus/HLSion). and basically it consists of 3 parts:
SessionManager (Which managed all of sessions)
HLSData (HLSData model which initialized same as the code below. it is like an intermediary between the session manager )
public convenience init(url: URL, options: [String: Any]? = nil, name: String) {
let urlAsset = AVURLAsset(url: url, options: options)
self.init(asset: urlAsset, description: name)
}
AssetStore (It's managed HLSData.plist file. Which contain name and path of each download session).
this is how the start of downloads is implemented:
var sources = [HLSData]()
#objc func startDownloadButtonTapped() {
print(#function)
let hlsData = sources[0]
switch hlsData.state {
case .notDownloaded:
hlsData.download { (percent) in
DispatchQueue.main.async {
print("percent = \(percent)")
self.percentLabel.text = "\(percent)"
}
}.finish { (relativePath) in
DispatchQueue.main.async {
print("download completed relative path = \(relativePath)")
}
}.onError { (error) in
print("Error finish. \(error)")
}
case .downloading:
print("State is downloading")
break
case .downloaded:
print(hlsData.localUrl ?? "localURL is nil")
}
}
Before tapping state is notDownloaded. respectively app is start download when the button tapped and state is changed to downloading.
Everything is works fine and progress tracked well. But when i go to the background and return back to app, state is still keep of downloading, but progress closure doesn't work anymore. How can i restore or reset this closures for tracking progress. Thanks in advance.
On doing some tests, I feel there is a bug in iOS 12 and below with the AVAssetDownloadDelegate
When doing some tests, I noticed the following when trying to download media over HLS using AVAssetDownloadTask:
iOS 13 and above
When going into the background, the download continues
When coming into the foreground from the background, the AVAssetDownloadDelegate still triggers assetDownloadTask didLoad totalTimeRangesLoaded and the progress can be updated
After suspending or quitting an app, reinitializing an AVAssetDownloadURLSession with the same URLSessionConfiguration identifier, the download resumes automatically from where it last left off
iOS 12 and below
Everything still almost holds true except point 2, for some reason the assetDownloadTask didLoad totalTimeRangesLoaded no longer gets triggered when coming into the foreground from the background and so the progress no longer gets updated.
One workaround I got was from this answer https://stackoverflow.com/a/55847387/1619193 was that in the past, downloads had to be resumed manually after the app was suspended for AVAssetDownloadTask by providing it a location to the partially downloaded file on disk.
As per the documentation:
AVAssetDownloadTask provides the ability to resume previously stopped
downloads under certain circumstances. To do so, simply instantiate a
new AVAssetDownloadTask with an AVURLAsset instantiated with a file
NSURL pointing to the partially downloaded bundle with the desired
download options, and the download will continue restoring any
previously downloaded data.
Interestingly, you cannot find this on the official documentation anymore and also it seems like setting the destinationURL has been deprecated so it seems like there has been some refactoring in how things work.
My solution:
Subscribe to the UIApplication.willEnterForegroundNotification notification
In the call back for the UIApplication.willEnterForegroundNotification, check if the device is running iOS 12 and below
If it does, cancel the current AVAssetDownloadTask
This should trigger the AVAssetDownloadDelegate callback assetDownloadTask didFinishDownloadingTo which will give you the location of the partially downloaded file
Reconfigure the AVAssetDownloadTask but do not configure it with the HLS url, instead configure it with the URL to the partially downloaded asset
Resume the download and the progress AVAssetDownloadDelegate will seem to start firing again
You can download an example of this here
Here are some small snippets of the above steps:
private let downloadButton = UIButton(type: .system)
private let downloadTaskIdentifier = "com.mindhyve.HLSDOWNLOADER"
private var backgroundConfiguration: URLSessionConfiguration?
private var assetDownloadURLSession: AVAssetDownloadURLSession!
private var downloadTask: AVAssetDownloadTask!
override func viewDidLoad()
{
super.viewDidLoad()
// UI configuration left out intentionally
subscribeToNotifications()
initializeDownloadSession()
}
private func initializeDownloadSession()
{
// This will create a new configuration if the identifier does not exist
// Otherwise, it will reuse the existing identifier which is how a download
// task resumes
backgroundConfiguration
= URLSessionConfiguration.background(withIdentifier: downloadTaskIdentifier)
// Resume will happen automatically when this configuration is made
assetDownloadURLSession
= AVAssetDownloadURLSession(configuration: backgroundConfiguration!,
assetDownloadDelegate: self,
delegateQueue: OperationQueue.main)
}
private func resumeDownloadTask()
{
var sourceURL = getHLSSourceURL(.large)
// Now Check if we have any previous download tasks to resume
if let destinationURL = destinationURL
{
sourceURL = destinationURL
}
if let sourceURL = sourceURL
{
let urlAsset = AVURLAsset(url: sourceURL)
downloadTask = assetDownloadURLSession.makeAssetDownloadTask(asset: urlAsset,
assetTitle: "Movie",
assetArtworkData: nil,
options: nil)
downloadTask.resume()
}
}
func cancelDownloadTask()
{
downloadTask.cancel()
}
private func getHLSSourceURL(_ size: HLSSampleSize) -> URL?
{
if size == .large
{
return URL(string: "https://video.film.belet.me/45505/480/ff27c84a-6a13-4429-b830-02385592698b.m3u8")
}
return URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8")
}
// MARK: INTENTS
#objc
private func downloadButtonTapped()
{
print("\(downloadButton.titleLabel!.text!) tapped")
if downloadTask != nil,
downloadTask.state == .running
{
cancelDownloadTask()
}
else
{
resumeDownloadTask()
}
}
#objc
private func didEnterForeground()
{
if #available(iOS 13.0, *) { return }
// In iOS 12 and below, there seems to be a bug with AVAssetDownloadDelegate.
// It will not give you progress when coming from the background so we cancel
// the task and resume it and you should see the progress in maybe 5-8 seconds
if let downloadTask = downloadTask
{
downloadTask.cancel()
initializeDownloadSession()
resumeDownloadTask()
}
}
private func subscribeToNotifications()
{
NotificationCenter.default.addObserver(self,
selector: #selector(didEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
// MARK: AVAssetDownloadDelegate
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?)
{
guard error != nil else
{
// download complete, do what you want
return
}
// something went wrong, handle errors
}
func urlSession(_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didFinishDownloadingTo location: URL)
{
// Save the download path of the task to resume downloads
destinationURL = location
}
If something seems out of place, I recommend checking out the full working example here

download file with urlsesssion

I'm trying to download using urlsession background session this is my main function
func startfresh() {
session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
let url = URL(string: "https://nava.ir/wp-content/uploads/2018/08/Gholamreza-Sanatgar-Dorooghe-Sefid-128.mp3")
task = session.downloadTask(with: url!)
task.resume()
}
and my didcompletewitherror
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
let err = error as NSError?
let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
print("anotherone")
let newtask = session.downloadTask(withResumeData: resumeData!)
newtask.resume()
}
else {
print("hichi")
}
}
but when I close the app when the download is still on progress and relaunch it again and press start download it starts 2 tasks resume previous one and start a new one I want to just resume the previous one with resume data what should I do to just trigger did complete with error method.
What you are seeing is kind of "expected" and you have to design your software to handle it. Actually, there are some more things you should consider. I've investigated and put down at the next as an answer. (NSURLSessionDownloadTask move temporary file)
A sample project is also available.

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

Apple Sample Code for WatchKit Extension Background Refresh

I've been searching for the answer to this question for quite a long time, so I think I'm willing to risk some downvotes to post it.
Basically, I want to make the Apple provided sample code for Apple Watch background refresh actually work (link and code below).
I've tried both in the simulator and on an iPhone 6s with an Apple Watch Series 2, and the background tasks are never successfully completed to the point where the time updates. I've tried pinning the watch app to the dock, and I've tried keeping the app in the foreground and sending it to the background, both in the simulator and on the actual watch. I even tried waiting almost a year to see if Xcode or the Apple Watch would receive an update that would make it work.
Has anyone successfully modified the Apple provided code to make it work?
You can download the entire runnable sample project here: WatchBackgroundRefresh: Using WKRefreshBackgroundTask to update WatchKit apps in the background
/*
Copyright (C) 2016-2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this sample’s licensing information
Abstract:
The main interface controller.
*/
import WatchKit
import Foundation
class MainInterfaceController: WKInterfaceController, WKExtensionDelegate, URLSessionDownloadDelegate {
// MARK: Properties
let sampleDownloadURL = URL(string: "http://devstreaming.apple.com/videos/wwdc/2015/802mpzd3nzovlygpbg/802/802_designing_for_apple_watch.pdf?dl=1")!
#IBOutlet var timeDisplayLabel: WKInterfaceLabel!
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .long
return formatter
}()
// MARK: WKInterfaceController
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Configure interface objects here.
WKExtension.shared().delegate = self
updateDateLabel()
}
// MARK: WKExtensionDelegate
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task : WKRefreshBackgroundTask in backgroundTasks {
print("received background task: ", task)
// only handle these while running in the background
if (WKExtension.shared().applicationState == .background) {
if task is WKApplicationRefreshBackgroundTask {
// this task is completed below, our app will then suspend while the download session runs
print("application task received, start URL session")
scheduleURLSession()
}
}
else if let urlTask = task as? WKURLSessionRefreshBackgroundTask {
let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: urlTask.sessionIdentifier)
let backgroundSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil)
print("Rejoining session ", backgroundSession)
}
// make sure to complete all tasks, even ones you don't handle
task.setTaskCompleted()
}
}
// MARK: Snapshot and UI updating
func scheduleSnapshot() {
// fire now, we're ready
let fireDate = Date()
WKExtension.shared().scheduleSnapshotRefresh(withPreferredDate: fireDate, userInfo: nil) { error in
if (error == nil) {
print("successfully scheduled snapshot. All background work completed.")
}
}
}
func updateDateLabel() {
let currentDate = Date()
timeDisplayLabel.setText(dateFormatter.string(from: currentDate))
}
// MARK: URLSession handling
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("NSURLSession finished to url: ", location)
updateDateLabel()
scheduleSnapshot()
}
func scheduleURLSession() {
let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: NSUUID().uuidString)
backgroundConfigObject.sessionSendsLaunchEvents = true
let backgroundSession = URLSession(configuration: backgroundConfigObject)
let downloadTask = backgroundSession.downloadTask(with: sampleDownloadURL)
downloadTask.resume()
}
// MARK: IB actions
#IBAction func ScheduleRefreshButtonTapped() {
// fire in 20 seconds
let fireDate = Date(timeIntervalSinceNow: 20.0)
// optional, any SecureCoding compliant data can be passed here
let userInfo = ["reason" : "background update"] as NSDictionary
WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: fireDate, userInfo: userInfo) { (error) in
if (error == nil) {
print("successfully scheduled background task, use the crown to send the app to the background and wait for handle:BackgroundTasks to fire.")
}
}
}
}
The following is output when run on the simulator. Similar output (but not necessarily exactly the same) when running in other configurations:
successfully scheduled background task, use the crown to send the app to the background and wait for handle:BackgroundTasks to fire.
received background task: <WKSnapshotRefreshBackgroundTask: 0x7b019030>
received background task: <WKApplicationRefreshBackgroundTask: 0x7a711290>
application task received, start URL session
For anyone who may find this, there were 2 problems that I saw, both with the URLSession scheduling. With these changes, I think the Apple sample code actually works, at least on the simulator.
-The sampleDownloadURL needs to be secure, so a URL with HTTPS is necessary. This one works: https://api.weather.gov/points/42.3584,-71.0598/forecast
-It looks to me like the delegate for the URLSession was never set to self, so the following change fixed that:
let backgroundSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil)
The following working (although less complete) code was very helpful: What's New in watchOS 3: Background Tasks
Edit: One more issue that may occur is that the tasks are completed immediately after they are received. In practice, they should be saved (in a local variable, for instance) and then completed after all processing for that task is complete. For this code, I think that means hanging onto the WKApplicationRefreshBackgroundTask and not calling setTaskCompleted() on it until right after the call to scheduleSnapshot.

swift 3+, URLsession, in the background fails apparently at random

I am fairly new to swift(1 week) and iOS programming, and my problem is that I seem to miss some basic understanding. Below you see a function that is triggered by a background notification. I can and have verified that I receive the background notification reliably and the app comes active (printout of the raw data values on the console) As long as the app is in the foreground everything is working just as expected, it gets fired, and sends a single https request. The background triggers come on a timer every minute.
Now the whole thing changes when the app enters into the background. In this case I am still getting the triggers through the notification (console printout) and I can see in the debugger the same function that works like a charm in the foreground stumbles. It still works, it still gets fired, but a data packet is sent only so often, randomly as it seems between 2 and 30 minutes.
let config = URLSessionConfiguration.background(withIdentifier: "org.x.Reporter")
class queryService {
let defaultSession = URLSession(configuration: config)
var dataTask: URLSessionDataTask?
var errorMessage = ""
func getSearchResults(baseURL: String, searchTerm: String) {
dataTask?.cancel()
config.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData;
config.timeoutIntervalForRequest = 10
if var urlComponents = URLComponents(string: "https://host.com/reportPosition.php") {
urlComponents.query = "\(searchTerm)"
guard let url = urlComponents.url else { return }
dataTask = defaultSession.dataTask(with: url)
}
// 7
dataTask?.resume()
}
}
Try using dataTaskWithCompletion so you can see what's going wrong in the error.
URLSession.shared.dataTask(with: URL.init(string: "")!) { (data, response, error) in
if error != nil {
// Error
}
}.resume()
https://developer.apple.com/documentation/foundation/urlsession/1410330-datatask
EDIT
What you want to do is for background you get completions via delegate call backs so when you init ur URLSession do so using the following func
URLSession.init(configuration: URLSessionConfiguration.init(), delegate: self, delegateQueue: OperationQueue.init())
https://developer.apple.com/documentation/foundation/urlsession/1411597-init
Then conform ur class to the URLSessionDelegate like so
class queryService, URLSessionDelegate {
then implement the delegate methods listed here for call backs
https://developer.apple.com/documentation/foundation/urlsessiondelegate
EDIT2
Here is good tutorial about it
https://www.raywenderlich.com/158106/urlsession-tutorial-getting-started

Resources