Trouble with getting a background refresh task working

I am able to setup and schedule a background refresh task as well as manually trigger it via Xcode and the simulator tied to my iPhone 11 Pro test phone using the e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier: However, it won't execute on the app on it's own. And yes, the pinfo.list entries are correct and match!

I know the scheduler is not exact on timing but it's just not executing on its own. Since I can trigger manually it I'm pretty sure the code is good but I must be missing something.

I created an observable object for this code and the relevant parts look like this:

class BackgroundTaskHandler: ObservableObject {
    
    static let shared = BackgroundTaskHandler()
    
    var taskState: BackgroundTaskState = .idle
    
    let backgroundAppRefreshTask = "com.opexnetworks.templateapp.shell.V1.appRefreshTask"
    
    func registerBackgroundTask() {
        
        BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundAppRefreshTask, using: nil) { task in
            self.handleAppRefresh(task: task as! BGAppRefreshTask)
        }
        self.updateTaskState(to: .idle, logMessage: "✅ Background app refresh task '\(backgroundAppRefreshTask)' registered.")
        
        BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in
            self.handleProcessingTask(task: task as! BGProcessingTask)
        }
        self.updateTaskState(to: .idle, logMessage: "✅ Background task identifier '\(backgroundTaskIdentifier)' registered.")
    }
    
    // Handle the app refresh task
    private func handleAppRefresh(task: BGAppRefreshTask) {
        self.updateTaskState(to: .running, logMessage: "🔥 app refresh task is now running.")
        PostNotification.sendNotification(title: "Task Running", body: "App refresh task is now running.")
        
        let queue = OperationQueue()
        queue.maxConcurrentOperationCount = 1
        
        let operation = BlockOperation {
            self.doSomeShortTaskWork()
        }
        
        task.expirationHandler = {
            self.updateTaskState(to: .expired, logMessage: "💀 App refresh task expired before completion.")
            PostNotification.sendNotification(title: "Task Expired", body: "App refresh task expired before completion \(self.formattedDate(Date())).")

            operation.cancel()
        }
        
        operation.completionBlock = {
            if !operation.isCancelled {
                self.taskState = .completed
            }
            task.setTaskCompleted(success: !operation.isCancelled)
            let completionDate = Date()
            UserDefaults.standard.set(completionDate, forKey: "LastBackgroundTaskCompletionDate")
            self.updateTaskState(to: .completed, logMessage: "🏁 App refresh task completed at \(self.formattedDate(completionDate)).")
            PostNotification.sendNotification(title: "Task Completed", body: "App refresh task completed at: \(completionDate)")
            
            self.scheduleAppRefresh() // Schedule the next one
        }
        
        queue.addOperation(operation)
    }

    func scheduleAppRefresh() {
        // Check for any pending task requests
        BGTaskScheduler.shared.getPendingTaskRequests { taskRequests in
            let refreshTaskIdentifier = self.backgroundAppRefreshTask
            let refreshTaskAlreadyScheduled = taskRequests.contains { $0.identifier == refreshTaskIdentifier }
            
            if refreshTaskAlreadyScheduled {
                self.updateTaskState(to: .pending, logMessage: "⚠️ App refresh task '\(refreshTaskIdentifier)' is already pending.")
                
                // Iterate over pending requests to get details
                for taskRequest in taskRequests where taskRequest.identifier == refreshTaskIdentifier {
                    let earliestBeginDate: String
                    if let date = taskRequest.earliestBeginDate {
                        earliestBeginDate = self.formattedDate(date)
                    } else {
                        earliestBeginDate = "never"
                    }
                    self.updateTaskState(to: .pending, logMessage: "⚠️ Pending Task: \(taskRequest.identifier), Earliest Begin Date: \(earliestBeginDate)")
                }
                
                // Optionally, show a warning message to the user in your app
                PostNotification.sendNotification(title: "Pending Tasks", body: "App refresh task is already pending. Task scheduling cancelled.")
                return
            } else {
                // No pending app refresh task, so schedule a new one
                let request = BGAppRefreshTaskRequest(identifier: refreshTaskIdentifier)
                request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // Earliest in 15 minutes
                
                do {
                    try BGTaskScheduler.shared.submit(request)
                    self.taskState = .scheduled
                    self.updateTaskState(to: .scheduled, logMessage: "✅ App refresh task '\(refreshTaskIdentifier)' successfully scheduled for about 15 minutes later.")
                    PostNotification.sendNotification(title: "Task Scheduled", body: "App refresh task has been scheduled to run in about 15 minutes.")
                } catch {
                    print("Could not schedule app refresh: \(error)")
                    self.taskState = .failed
                    self.updateTaskState(to: .failed, logMessage: "❌ Failed to schedule app refresh task.")
                }
            }
        }
    }
    
    // Short task work simulation
    private func doSomeShortTaskWork() {
        print("Doing some short task work...")
        // Simulate a short background task (e.g., fetching new data from server)
        sleep(5)
        print("Short task work completed.")
    }

In my AppDelegate I trigger the registerBackground task in the didFinishLaunchingWithOptions here:

BackgroundTaskHandler.shared.registerBackgroundTask()

And I scheduled it here in the launch view under a task when visible:

.task {
       BackgroundTaskHandler.shared.scheduleAppRefresh()
}
                    

I've also tried the last in the AppDelegate after registering. either way the task schedules but never executes.

Answered by mungbeans in 805928022

There's no point setting the minimum time to 15 minutes and then sitting watching it and assuming it'll trigger after 15 minutes, or not long after that. It might not be until a few or several hours later, probably during the night in my experience , have you tried waiting that long?

There's no point setting the minimum time to 15 minutes and then sitting watching it and assuming it'll trigger after 15 minutes, or not long after that. It might not be until a few or several hours later, probably during the night in my experience , have you tried waiting that long?

There's no point setting the minimum time to 15 minutes and then sitting watching it

Right.

I have a post, iOS Background Execution Limits, that outlines the sorts of behaviour you can expect from an app refresh task.

Share and Enjoy

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

Accepted Answer

Turns out my code is good so it's just a matter of the ambiguity of running background and app refresh tracks. After 12 hours the tasks started executing on cycle. We can close this one out.

Trouble with getting a background refresh task working
 
 
Q