Loren Kuich
Loren Kuich

Reputation: 603

iOS - Running a network request in the background with app closed

I'm currently working on a cross-platform (native Java and Swift/Objective-C) mobile app where the users may frequently have limited access to a network connection, but we want to record and send up statistics when a network becomes available, even if our app has been closed. We’ve already done this on Android fairly easily with their AndroidX.WorkManager library like so:

String URL = "https://myexampleurl.com";
Data inputData = new Data.Builder()
    .putString("URL", URL)
    .build();

Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();

OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(NetworkWorker.class)
    .setInputData(inputData)
    .setConstraints(constraints)
    .build();

WorkManager.getInstance(context).enqueueUniqueWork("com.lkuich.exampleWorker", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest);

This works great, when our app invokes this method, our request is added to a queue, and even if the user is offline and closes our app, when they reconnect to a network our request still gets made.

I’ve been struggling however to find an iOS equivalent to this API, and I am fairly new to native iOS development. We've enabled the Background Task capability and registered our identifiers in Plist.info in our project.

First we’ve tried the URLSession "background" API like so:

class NetworkController: NSObject, URLSessionTaskDelegate {
    func buildTask() -> URLSession {
        let config = URLSessionConfiguration.background(withIdentifier: "com.lkuich.exampleWorker")
        config.waitsForConnectivity = true
        config.shouldUseExtendedBackgroundIdleMode = true
        
        return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
    }
    
    func makeNetworkRequest(session: URLSession) {
        let url = URL(string: "https://myexampleurl.com")!
        
        let task = session.dataTask(with: url)
        task.resume()
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print("We're done here")
    }
}

This works well when the WiFi is disabled and re-enabled, but if the app is closed the request never gets sent it seems.

So we tried the BGTaskScheduler instead, but I'm having a hard time testing if it's working in the background with the app closed, since it can often take hours to actually run (forcing it to run immediately with a network connection works):

static let bgIdentifier = "com.lkuich.exampleWorker"

// Invoked on app start (didFinishLaunchingWithOptions)
static func registerBackgroundTasks() {
    BGTaskScheduler.shared.register(forTaskWithIdentifier: bgIdentifier, using: nil) { (task) in        
        task.expirationHandler = {
            task.setTaskCompleted(success: false)
        }
        
        // Make my network request in BG
        let url = URL(string: "https://myexampleworker.com")!
        let t = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
            // Print the response
            print(response)

            task.setTaskCompleted(success: true)
        })
        t.resume()
    }
}

static func makeRequest() {
    do {
        let bgRequest = BGProcessingTaskRequest(identifier: bgIdentifier)
            bgRequest.requiresNetworkConnectivity = true
            bgRequest.requiresExternalPower = false
            
        try BGTaskScheduler.shared.submit(bgRequest)
        print("Submitted task request")
    } catch {
        print("Failed to submit BGTask")
    }
}

Is it possible to schedule a network request to be run in the background when a network is available, even if the invoking app is closed? And if so, how would I pass input data (like URL args for example) to my background task, since my task has to be registered on app start?

Upvotes: 3

Views: 4676

Answers (1)

andrija
andrija

Reputation: 342

There are two possible states a closed app can be in. It's either in the background, or it is killed. App can be killed by the system, for various reasons, or by the user, who can kill it from the app switcher.

This is important because when the app is killed, you can't do anything in the background. Only VoIP apps are exception (using PushKit, which you are now allowed to be using if the app doesn't have VoIP functionality, you won't get pass the AppStore), and watchOS complications. But for all other apps, when the app is killed you have no options at all. That is just Apple's standard, they don't want apps that are not running to be able to run code for security reasons.

You can register some tasks to be done, when the app is still in the background, using the BGTask API you mentioned. I used that same API for one of my apps, and found it extremely volatile. Sometimes a task is done every 30 minutes, and then it won't be executed until tomorrow morning, then at one time it would just stop getting executed at all.

Therefore I think that silent push notifications are the best way out, but they also only work when the app is still in the background, not killed. Unless your app has VoIP functionality, in which case you can use PushKit.

Upvotes: 2

Related Questions