URL session during background fetch

My app uses web data. When running in the foreground, the following code works very nicely. (I've defined it in the view controller class but also in a separate Central.swift file. Same behavior.)


It also works during a background fetch, but only sporadically. It's not 100% reliable:


    var urlString: String = "some website"
    if let url = URL(string: urlString) {
        var url = URL(string: urlString)
        var request = URLRequest(url: url!)
        request.httpMethod = "GET"
        request.addValue("text/html", forHTTPHeaderField: "Content-Type")
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
               print("The Error is \(error)")
            if data != nil {
                var return1 = NSString(data: data!, encoding: String.Encoding.utf8.rawValue) as! String
//     Process the web data here
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                    var defaults: UserDefaults = UserDefaults.standard
                    defaults.set(myNumber, forKey: "myKey")
                    defaults.set(Date(), forKey: "myUpdate")
                    self.UILabel?.text =  x
                   Central().someFunction()  
//  This function is defined in the class Central.swift. It uses data stored in user defaults.
//   It sends a notification if the app state is inactive.

                }
            }
        }
        task.resume()
    }


When it fails, it seems to be stuck just before the DispatchQueue statement. Sometimes everything inside the Dispatch will execute when I bring the app to the foreground, instead of executing in the background beforehand. When it works, the commands in Dispatch execute normally, including the sending of an an alert notification to the user that's called in the Central().someFunction. This tells the user that new data has arrived.


Should the Session be something besides URLSession.shared ? What else?


This is really making me nuts. As I write this, it just worked. But I know it often will not.


BTW, I do my testing with my iPhone 6, not iOS simulator. Part of my confusion is that printing to the console may fail even when the code seems to have otherwise worked.


PS: I add a print to line #08 above and get this on the console:

The Error is Optional(Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSUnderlyingError=0x170052bd0 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, NSErrorFailingURLStringKey= some website, NSErrorFailingURLKey=some website, _kCFStreamErrorDomainKey=4, _kCFStreamErrorCodeKey=-2102, NSLocalizedDescription=The request timed out.})


A similar discussion is here: https://forums.developer.apple.com/thread/22032

Replies

Check out this post.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I've been studying the issue in that post but I'm still confused about which direction to go. My web tasks complete in just seconds, so the 30 second limit is not a problem for a user with good internet service. And I am not triggering (yet) from a push notification, so I thought a system-triggered backgound fetch was a good option.


Is my problem that the app is going into a suspended state, as opposed to merely in the background? I have the following in the AppDelegate:


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
            UNUserNotificationCenter.current().requestAuthorization(
                options: [.alert,.sound,.badge],
                completionHandler: { (granted,error) in
                }
            )
    return true
    }

    func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
        print("Calling up the Windows Fetch")
        Windows().fetch {    // "Windows" is a view controller
        }
        print("Calling up the Central Fetch")
        Central().fetch {    // "Central" is a .swift file


        completionHandler(.newData)
        }
    }


And I have added the UIBackgroundModes key to my Info.plist file with both "App downloads..." option items.


I thought this was enough to allow background fetches to be triggered by the iOS at any time. It seems to work at least part of the time. The Apple documentation says:


Fetching Small Amounts of Content Opportunistically

Apps that need to check for new content periodically can ask the system to wake them up so that they can initiate a fetch operation for that content. To support this mode, enable the Background fetch option from the Background modes section of the Capabilities tab in your Xcode project. (You can also enable this support by including the

UIBackgroundModes
key with the
fetch
value in your app’s
Info.plist
file.) Enabling this mode is not a guarantee that the system will give your app any time to perform background fetches. The system must balance your app’s need to fetch content with the needs of other apps and the system itself. After assessing that information, the system gives time to apps when there are good opportunities to do so.


As I understand that, my app should occasionally be awoken from even a suspended state, into the background where a quick fetch can be processed. And in fact it works some of the time. But more often, I'm getting the "timed out" error above.

Well today is a brighter day. I installed Xcode 8.2 beta 8C23 last night and things seem to be working far more reliably today. I'm not saying that's cause and effect, but at least a coincidence.


I've noticed also that the "Comment Selection" menu command is now functioning again. This was not working in Xcode 8.1 for me and many others. I was able to temporarily fix it by renaming the app (by appending the version 8.1 to the app file name) but the problem soon returned. Maybe my Xcode installation was somehow buggered, and installing the beta fixed it. Whatever, it seems to be working for now.


The error is still happening, just at a much lower rate. Maybe it has something to do with my network? Still looking for a way to quash this.


Domain=NSURLErrorDomain Code=-1005 "The network connection was lost."

After the previous post I've continued to have a high failure rate and have now started to adopt the background download task approach instead of the data task. I'm still working on it but I'm optimisitc it will be up and running soon.


In the meanwhile I've seen quite a few errors about reading and writing to the user defaults while in the background. I came across this thread:

https://forums.developer.apple.com/thread/15685


Do you still advise agaisnt using UserDefaults during background operation? It's the only method I've learned so far for saving data, so I'm reluctant to give it up. I need to process the web data from my session and place a few things into storage. Should I not use UserDefaults ?

Do you still advise [against] using UserDefaults during background operation?

User defaults was intended for storing things like preferences, which are small and not particularly significant (that is, if they get dropped it’s not the end of the world). If that describes your needs, it’s fine to use user default in general.

The obvious gotcha is background execution. If your app might run in a situation where the files backing user defaults are not available, you will run into problems.

It's the only method I've learned so far for saving data …

Look on the bright side, it’s an opportunity to learn something new!

Seriously though, putting this data into a file won’t be too difficult. You don’t need anything complex:

  • use

    PropertyListSerialization
    to turn simple data (dictionary, array, string, and so on) into
    Data
  • use

    FileManager
    to get a
    URL
    for the Documents directory
  • use

    URL.appendPathComponent()
    to get a URL for your file
  • read and write that using

    Data(contentsOf:)
    and
    write(to:)

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"