Automatic Background File Uploads

I have currently created an app which contains an upload button which when clicked upload health data using HealthKit to an AWS S3 bucket.

Now I want to implement an automatic file upload mechanism which would mean that the app is installed and opened just once - and then the upload must happen on a schedule (once daily) from the background without ever having to open the app again.

I've tried frameworks like NSURLSession and BackgroundTasks but nothing seems to work. Is this use case even possible to implement? Does iOS allow this?

The file is just a few KBs in size.

For reference, here is the Background Tasks code:

import UIKit
import BackgroundTasks
import HealthKit

class AppDelegate: NSObject, UIApplicationDelegate {
    let backgroundTaskIdentifier = "com.yourapp.healthdata.upload"
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Register the background task
        BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in
            self.handleHealthDataUpload(task: task as! BGAppRefreshTask)
        }
        // Schedule the first upload task
        scheduleDailyUpload()
        return true
    }

    // Schedule the background task for daily execution
    func scheduleDailyUpload() {
        print("[AppDelegate] Scheduling daily background task.")
        let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier)
        request.earliestBeginDate = Date(timeIntervalSinceNow: 24*60*60)
        do {
            try BGTaskScheduler.shared.submit(request)
            print("[AppDelegate] Daily background task scheduled.")
        } catch {
            print("[AppDelegate] Could not schedule daily background task: \(error.localizedDescription)")
        }
    }

    // Handle the background task when it's triggered by the system
    func handleHealthDataUpload(task: BGAppRefreshTask) {
        print("[AppDelegate] Background task triggered.")
        // Call your upload function with completion handler
        HealthStoreManager.shared.fetchAndUploadHealthData { success in
            if success {
                print("[AppDelegate] Upload completed successfully.")
                task.setTaskCompleted(success: true)
                // Schedule the next day's upload after a successful upload
                self.scheduleDailyUpload()
            } else {
                print("[AppDelegate] Upload failed.")
                task.setTaskCompleted(success: false)
            }
        }
        // Handle task expiration (e.g., if upload takes too long)
        task.expirationHandler = {
            print("[AppDelegate] Background task expired.")
            task.setTaskCompleted(success: false)
        }
    }
}
Answered by DTS Engineer in 826878022
Written by DavidBenGurion in 775338021
the upload must happen on a schedule (once daily) from the background without ever having to open the app again.

The issue here is that you’re using an app refresh task. App refresh only kicks in if the user is using your app frequently. I talk about this more in iOS Background Execution Limits.

What you want is a processing task. Those usually run once a day, over night, when the device is on mains power and has access to the network.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Written by DavidBenGurion in 775338021
the upload must happen on a schedule (once daily) from the background without ever having to open the app again.

The issue here is that you’re using an app refresh task. App refresh only kicks in if the user is using your app frequently. I talk about this more in iOS Background Execution Limits.

What you want is a processing task. Those usually run once a day, over night, when the device is on mains power and has access to the network.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you for this! I have a very similar use case where we need to upload heath data nightly. I just changed our implementation to use BGProcessingTaskRequest, however it still didn't seem to work. We use SwiftUI. Please note that the following code is written for testing, so it is scheduled to be 1 minute out. And since I am running this often, I have the App Delegate cancel previous tasks before registering/scheduling a new one. In real life, we will have it scheduled for 3 AM daily, and the app delegate will check if a task is already scheduled and, if not, schedule it. Also note that we have user consent and permissions enabled and this process is handled elsewhere.

In our AppDelegate class, we have:


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {

self.healthKitStuff()
return true

}

    func healthKitStuff() {
        BGTaskScheduler.shared.getPendingTaskRequests { result in
            let alreadyScheduled = result.contains { $0.identifier == Tasks.healthKitDataSync.rawValue }
            if !alreadyScheduled {
                HealthKit.shared.registerHealthKitSyncJob()
                HealthKit.shared.scheduleHealthKitSyncJob()
            } else {
                HealthKit.shared.cancelHealthKitSyncJob()
                self.healthKitStuff()
            }
      }
    }
    

In our HealthKit.swift file:

import Foundation
import BackgroundTasks
import HealthKit
import SwiftUI

class HealthKit: NSObject {

    static let shared = HealthKit()

    // identifier defined in info.plist -> 'Permitted background task scheduler identifiers'

    private var isSupported: Bool {
        return HKHealthStore.isHealthDataAvailable()
    }
}

// Sync HealthKit data with server

extension HealthKit {

    func uploadYesterdaysHealthData(completionHandler: @escaping () -> Void) {

        print("***7 in uploadYesterdaysHealthData")

        fetchYesterdaysHealthData() { input in

            print("input is : \(input)")

            let mutation = SaveFitnessDataMutation(input: input)

            Api.shared.apollo.perform(mutation: mutation) { result in

                completionHandler()

                switch result {

                case .success(let data):

                    if let resultData = data.data {

                        print("START printing result data")

                        print(resultData.saveUserFitness.success)

                        print(resultData.saveUserFitness.message)

                        print("END printing result data")

                    }

                    if let errors = data.errors {

                        print("Error! : \(errors)")

                    }

                case .failure(let error):

                    print("Error! : \(error)")

                }

            }

        }

    }

    

    func registerHealthKitSyncJob() {

        print("***4 in registerHealthKitSyncJob")

        BGTaskScheduler.shared.register(forTaskWithIdentifier: Tasks.healthKitDataSync.rawValue, using: nil) { task in

            print("***5 task is: \(task)")

            HealthKit.shared.uploadYesterdaysHealthData() {

                print("***6 task completed")

                task.setTaskCompleted(success: true)

                // call the job again to run the next day

                print("calling scheduleHealthKitSyncJob from register")

                self.scheduleHealthKitSyncJob()

            }

        }

    }

    

    func cancelHealthKitSyncJob() {

        print("CALLING CANCEL")

        BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: Tasks.healthKitDataSync.rawValue)

    }

    

    func scheduleHealthKitSyncJob() {

        print("zzz3 in scheduleHealthKitSyncJob")

        //let request = BGAppRefreshTaskRequest(identifier: Tasks.healthKitDataSync.rawValue)

        let request = BGProcessingTaskRequest(identifier: Tasks.healthKitDataSync.rawValue)

        print("request is \(request)")

        var runAt: Date {

            let calendar = Calendar.current

            let startOfDay = calendar.startOfDay(for: Date())

            let tomorrow = calendar.date(byAdding: .day, value: 1, to: startOfDay)

            let runAt = Date().adding(minutes: 1) //calendar.date(byAdding: .hour, value: 3, to: tomorrow!)

            // Scheduling job to run day after user consent since we look at previous day's data

            print("runAt is: \(String(describing: runAt))")

            return runAt

        }

        request.earliestBeginDate = runAt

        

        do {

            try BGTaskScheduler.shared.submit(request)

            print("submitted task request")

        } catch {

            print("Could not schedule HealthKit Sync Job: \(error)")

        }

        

        print("zzz3 END scheduleHealthKitSyncJob")

    }

}



extension Date {

    func adding(minutes: Int) -> Date {

        return Calendar.current.date(byAdding: .minute, value: minutes, to: self)!

    }

}

Thank you!

Some general notes…

Setting earliestBeginDate in that way won’t do what you want. Background processing tasks won’t even run every 3 minutes. You should expect them to run overnight.

I generally recommend that you not mess with earliestBeginDate at all. By adding this constraint, you could prevent your background task from when it otherwise might.

Logging with print(…) is pointless, because when you test this for real you won’t be attached with Xcode. I recommend that you adopt the system log API. See Your Friend the System Log for links to docs and so on.

It’s really important that you separate the three aspects of this problem:

  • The actual work you want to do in your background task (A).

  • The code that gets your background task running (B).

  • Combining the above for an integration test (C).

For A, that’s normal networking code and you should create and test it like you would any other networking code.

For C, I recommend that you not run your app from Xcode. When you run from Xcode, background execution behaves weirdly, because the debugger prevents your app from suspending. Instead, run the app from the Home screen and let it run overnight, and then confirm its overnight behaviour by looking at the system log.

That leaves B. For basic testing you can use the technique described in Starting and Terminating Tasks During Development. Once you get the basics working, confirm the overnight behaviour like you do C, except with a dummy task rather than A.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Folks, I almost missed DavidBenGurion’s question in the comments. It’s better reply as a reply. See Quinn’s Top Ten DevForums Tips for this and other titbits.

DavidBenGurion wrote:

Also, is being connected to power a mandatory requirement?

That’s a complex question.

Or can we set requiresExternalPower = False?

You certainly can, but you have to understand what that means.

That property is a constraint. If you set it to true, it means that you won’t run unless the contraint is satisfied. That doesn’t say anything about the reverse. If you set it to false, you’re saying that you don’t care whether you have external power or not. But that’s not the same as applying a constraint in the opposite direction. It just allows the background task scheduler to make an unconstrained choice, and that choice is informed by various other criteria.

Setting it to false is the right right option for most background processing tasks. It would make sense to set it to true if, for example, your task does a whole bunch of CPU work, work that would significantly affect the user’s battery life.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Automatic Background File Uploads
 
 
Q