Background App Refresh

Hi, I have a couple questions about background app refresh. First, is the function RefreshAppContentsOperation() where to implement code that needs to be run in the background? Second, despite importing BackgroundTasks, I am getting the error "cannot find operationQueue in scope". What can I do to resolve that? Thank you.

func scheduleAppRefresh() {
       let request = BGAppRefreshTaskRequest(identifier: "peaceofmindmentalhealth.RoutineRefresh")
       // Fetch no earlier than 15 minutes from now.
       request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
            
       do {
          try BGTaskScheduler.shared.submit(request)
       } catch {
          print("Could not schedule app refresh: \(error)")
       }
    }
    
    func handleAppRefresh(task: BGAppRefreshTask) {
       // Schedule a new refresh task.
       scheduleAppRefresh()


       // Create an operation that performs the main part of the background task.
       let operation = RefreshAppContentsOperation()
       
       // Provide the background task with an expiration handler that cancels the operation.
       task.expirationHandler = {
          operation.cancel()
       }


       // Inform the system that the background task is complete
       // when the operation completes.
       operation.completionBlock = {
          task.setTaskCompleted(success: !operation.isCancelled)
       }


       // Start the operation.
       operationQueue.addOperation(operation)
     }
    
    func RefreshAppContentsOperation() -> Operation {
        
    }
Answered by DTS Engineer in 850134022

I fear you’re getting distracted by the Operation side of this. Operation isn’t required by this API. Its presence in the docs is an artefact of history, in that those docs were written before Swift concurrency was a thing. If you were doing this today, you’d ignore Operation and just use Swift concurrency.

Similarly, the docs are within the UIKit, but that’s also not required by this API, and if you were doing it today you’d use SwiftUI.

So, here’s how I created an app that manages an app refresh task using Swift concurrency and SwiftUI:

  1. Using Xcode 16.4, I created a new project from the iOS > App template, choosing SwiftUI for my interface.

  2. I switched the Swift Language Version build setting to Swift 6. If you’re gonna do Swift concurrency, you might as well have the compiler enforce the rules!

  3. I created an AppModel type to hold the state of my app:

    import Foundation
    import Observation
    
    @Observable
    @MainActor
    final class AppModel {
    
        private(set) var lastUpdate: Date? = nil
    
        private var isUpdating: Bool = false
        
        func updateNow() async {
            do {
                if self.isUpdating { return }
                self.isUpdating = true
                defer { self.isUpdating = false }
    
                try await Task.sleep(for: .seconds(3))
                self.lastUpdate = .now
            } catch {
                // discard errors
            }
        }
    }
    

    IMPORTANT In this app I’m using Task.sleep(for:) to simulate an update task that takes 3 seconds to complete. This supports cancellation, a fact that’ll come in handy below.

  4. I added a appModel property to the content view:

    struct ContentView: View {
        let appModel: AppModel
        …
    }
    
  5. This generated an error compiling the preview. That’s easy enough to fix, but I didn’t need to fix it for this app because I can only test it by running it on a real device. So, just for the sake of speed, I deleted the preview.

  6. I updated ContentView to display the last updated date and add a button that forces an update:

    struct ContentView: View {
        …
        var body: some View {
            VStack {
                HStack {
                    Text("Last Updated")
                    Text(appModel.lastUpdate.map { $0.description } ?? "never")
                }
                Button("Update") {
                    Task {
                        await appModel.updateNow()
                    }
                }
            }
            …
        }
    }
    
  7. I instantiated the app model in my App implementation, and passed it the ContentView initialiser:

    struct QAppRefreshApp: App {
        let appModel: AppModel = AppModel()
        var body: some Scene {
            WindowGroup {
                ContentView(appModel: appModel)
            }
        }
    }
    
  8. I ran the app on an iOS 18.5 device, just to make sure it works. Specifically, tapping Update delays for 3 seconds and then updates the Last Updated label.


With the basics in place, I added support for the app refresh task:

  1. I ran through the steps in Enable and schedule background tasks to set up the app refresh background mode and task identifier. My identifier is com.example.QAppRefresh.refresh.

  2. I added a start() method to my model that registers the handler:

    final class AppModel {
    
        // IMPORTANT: This must match the `Info.plist` value.
        
        let refreshTaskID = "com.example.QAppRefresh.refresh"
    
        func start() {
            BGTaskScheduler.shared.register(
                forTaskWithIdentifier: refreshTaskID,
                using: .main
            ) { task in
                self.handleAppRefresh()
            }
        }
    
        private func handleAppRefresh(appRefreshTask: BGAppRefreshTask) {
            // nothing yet
        }
    
        …
    }
    
  3. And called that in the app startup sequence:

    struct QAppRefreshApp: App {
        var appModel: AppModel = {
            let result = AppModel()
            result.start()
            return result
        }()
        …
    }
    
  4. I extended AppModel to schedule an app refresh:

    final class AppModel {
    
        private var isAppRefreshSchedule: Bool = false
    
        func scheduleAppRefresh() {
            if self.isAppRefreshSchedule { return }
            self.isAppRefreshSchedule = true
    
            do {
                let request = BGAppRefreshTaskRequest(
                    identifier: refreshTaskID
                )
                request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
                try BGTaskScheduler.shared.submit(request)
            } catch {
                // discard errors
            }
        }
    
        …
    }
    
  5. And added a Schedule button that calls the scheduleAppRefresh() method:

    struct ContentView: View {
        …
        var body: some View {
            VStack {
                …
                …
                Button("Schedule") {
                    appModel.scheduleAppRefresh()
                }
            }
            …
        }
    }
    
  6. I filled out the implementation of handleAppRefresh(…):

    final class AppModel {
    
        private func handleAppRefresh(appRefreshTask: BGAppRefreshTask) {
            self.isAppRefreshScheduled = false
            let swiftTask = Task {
                let success = await self.updateNow()
                appRefreshTask.setTaskCompleted(success: success)
                appRefreshTask.expirationHandler = nil
            }
            appRefreshTask.expirationHandler = {
                swiftTask.cancel()
            }
        }
        
        …
    }
    
  7. As I mentioned upthread, the only good way to debug this stuff is with logging, so I added a log handle:

    final class AppModel {
    
        let log = Logger(subsystem: "com.example.QAppRefresh", category: "refresh")
    
        …
    }
    
  8. And then added log points on all the relevant code paths. For example, this is what my scheduleAppRefresh() method actually looks like:

    final class AppModel {
        
        …
        
        func scheduleAppRefresh() {
            if self.isAppRefreshScheduled {
                self.log.debug("will not schedule, already scheduled")
                return
            }
            self.isAppRefreshScheduled = true
    
            do {
                self.log.debug("will schedule, task: \(self.refreshTaskID, privacy: .public)")
                … non-logging code elided …
                self.log.debug("did schedule")
            } catch {
                self.log.debug("did not schedule, error: \(error)")
            }
        }
    
        …
    }
    

    But that’s just an example. I added logging to all the code paths of all the methods that run during the refresh.


And with that, it’s time to test app refresh:

  1. I ran the app on my device.

  2. I tapped the Schedule button. It prints:

    will schedule, task: com.example.QAppRefresh.refresh
    did schedule
    
  3. I used the technique in Starting and Terminating Tasks During Development to simulate a refresh:

    (lldb) e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.QAppRefresh.refresh"]
    (lldb) continue
    
  4. I looked in the log and saw this:

    will run, task: com.example.QAppRefresh.refresh
    did run
    will update
    …
    did update
    

    IMPORTANT If you see No task request with identifier com.example.QAppRefresh.refresh has been scheduled, check Settings > General > Background App Refresh. Ask me how I know! |-:

With that working, I tested expiration:

  1. To make things easier, I increase the delay in updateNow() to 30 seconds.

  2. I repeated steps 1 through 3 of the previous list. This time the log shows:

    will run, task: com.example.QAppRefresh.refresh
    did run
    will update
    

    because the update is taking a long time.

  3. I used the technique in Starting and Terminating Tasks During Development to expire the task:

    (lldb) e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.example.QAppRefresh.refresh"]
    (lldb) continue
    
  4. I looked in the log again:

    did expire, task: com.example.QAppRefresh.refresh
    did not update, error: Swift.CancellationError()
    

    The app refresh task expired and that cancelled my update task.


Finally, a word about logging. In this example I kept Xcode attached to my app and simulated app refresh and expiration events. That’s fine for this test, but to debug this in real world conditions you have to run you app outside of Xcode. At that point the only way to work out what happened is with logging. I do my logging using the system log. See Your Friend the System Log for lots of advice about that.

In the above I logged using the debug(…) method. That’s fine for this example, but in the real world debug logging is not recorded unless some program like Console is looking for it. And even then it’s not persisted by default. In an app with complex background functionality, there’s an important trade-off:

  • If you log too little, you can’t debug your issues.
  • If you log too much, the system starts discarding your log entries.

Striking the correct balance requires some experimentation.

Share and Enjoy

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

You seem to be following some sort of tutorial or sample. Can you post a link to that? Because it’s hard to answer your questions without understanding that context. For example, you’ve decided to use an Operation to handle the request, but there’s no requirement to do that in the API, and so it’s hard to describe what’s up with operationQueue without that context.

Share and Enjoy

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

Indeed. I guess I should be able to recognise our own documentation code )-:

Anyway, with that context, I can answer your specific questions:

is the function RefreshAppContentsOperation() where to implement code that needs to be run in the background?

Yes and no. In this example RefreshAppContentsOperation is meant to be an Operation subclass that implements the app refresh operation. If you want to use this approach, you’d create your own subclass of Operation and then implement the main() method on that. For example:

final class RefreshAppContentsOperation: Operation {
    override func main() {
        … your code here …
    }
}

However, there are other approaches you might adopt. For example, you might start a Task and run Swift async code within that task.

Note The term task is overloaded in this context. The Background Tasks framework uses it to refer to the state that’s tracking the work and Swift concurrency uses it to refer to the state of the code that’s actually doing the work.

I am getting the error “cannot find operationQueue in scope”

Right. That’s because the doc assumes that you’re using Operation to managed this work. If you were doing that then you would have an operation queue lying around. If you’re not, then you have a choice:

  • You can use Operation as assumed by the doc. In that case, you’d need to create your own operation queue and supply it in the place of operationQueue.

  • You can use some other mechanism, in which case you won’t need an operation queue at all. For example, in Swift concurrency you can start a Task without worrying about where it starts (Swift concurrency has the concept of a global executor that runs tasks by default).

Historically, I was a big fan of Operation and I’d use it all the time for stuff like this. These days, however, I’d definitely do this work with Swift concurrency.

Share and Enjoy

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

I have the following code in ContentView:

.onAppear {
            BGTaskScheduler.shared.register(forTaskWithIdentifier: "appname.RoutineRefresh", using: nil) { task in
     print(task)
                
     handleAppRefresh(task: task as! BGAppRefreshTask)
  }
}

However, nothing prints. Additionally, I have print statements in scheduleAppRefresh and handleAppRefresh but nothing prints from those functions either.

I am also calling scheduleTaskRefresh in a .task function.

My general advice on this front is not to to do any of this work at the view level. Rather, this work should be done somewhere down in the model layer. So, it’s fine for activity in your UI to inform your model that it’s currently on screen and thus should try to update if possible, but all the update logic should be in your model. And, critically, these updates just change your model and your view responds to those changes in the same way it responds to any other changes.

Share and Enjoy

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

Could you elaborate on how to build a model layer and how that differentiates from calling a function from a .task or .onappear function within the view level? What do I need to pass to handleAppRefresh?

import Foundation
import SwiftData
import SwiftUI
import BackgroundTasks

func scheduleAppRefresh() {
   let request = BGAppRefreshTaskRequest(identifier: "app.RoutineRefresh")
   // Fetch no earlier than 15 minutes from now.
   request.earliestBeginDate = Date(timeIntervalSinceNow: 60)
        
   do {
      try BGTaskScheduler.shared.submit(request)
   } catch {
      print("Could not schedule app refresh: \(error)")
   }
}

func handleAppRefresh(task: BGAppRefreshTask) async {
   // Schedule a new refresh task.
   scheduleAppRefresh()


   // Create an operation that performs the main part of the background task.
   let operation = RefreshAppContentsOperation()
   
   // Provide the background task with an expiration handler that cancels the operation.
   task.expirationHandler = {
      operation.cancel()
   }


   // Inform the system that the background task is complete
   // when the operation completes.
   operation.completionBlock = {
      task.setTaskCompleted(success: !operation.isCancelled)
   }


   // Start the operation.
   //operationQueue.addOperation(operation)
 }


final class RefreshAppContentsOperation: Operation, @unchecked Sendable {
    @Environment(\.modelContext) private var modelContext
    
    override func main() {
        
    }
}
Could you elaborate on how to build a model layer … ?

Sure.

My preference is to have a passive model to which you can apply changes. The views can then observe the model and update accordingly. Then operations in the views result in changes to the model, which results in a one-way circular data flow: Views change the model and then react to changes in the model. So, you start with an architecture like this:

views
^   |
|   v
model

With that architecture, the views don’t have to be the only thing changing the model. You can, for example, build networking code that changes the model and the views then update to reflect those changes. The views don’t care that the change came from the network, they just update. So, adding your networking code results in this:

views
^   |
|   v
model <- network

In this diagram, the network item is often called a controller, although opinions differ as to whether that’s a correct use of that term.

Anyway, the point of this is that the views should never be in the business of dealing with the network. They are just about displaying the model. This allows makes it easy to test the views, display them in previews, and so on.

In this architecture, the business of app refresh doesn’t involve the views at all. Rather, you’d create another controller-y thing that manages the app refresh work and passes those instructions on to network. So, something like this:

views    refresh
^   |       |
|   v       v
model <- network

Share and Enjoy

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

Okay, thank you. And just clarifying, what needs to be passed to the handleAppRefresh function?

In the original article you referenced, handleAppRefresh(task:) is a function that you create and call in your BGTaskScheduler launch handler. It has two aspects:

  • It acts as a placeholder for whatever code you need to update your model.

  • It takes care of the ‘bookkeeping’ required by BGAppRefreshTask.

I can’t offer specific advice about the first aspect. That work is very much dependent on your model, your networking layer, and so on.

With regards the second aspect, that bookkeeping involves:

  • Calling setTaskCompleted(success:) when the update work completes.

  • Setting the expiration handler (expirationHandler) on the task, and making sure to cancel your update work if it expires.

  • Scheduling a subsequent app refresh task, if you determine one is necessary.

Share and Enjoy

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

With the code below I get the error:

class BackgroundTasksController: ObservableObject {
    func scheduleRoutineResetTask() {
        Task {
            BGTaskScheduler.shared.register(forTaskWithIdentifier: "app.BackgroundTask", using: nil) { task in
                Task {
                    await self.handleAppRefresh(task: task as! BGAppRefreshTask)
                }
            }
        }
    }
    
    private func handleAppRefresh(task: BGAppRefreshTask) async {
       // Schedule a new refresh task.
       scheduleAppRefresh()
        
       // Create an operation that performs the main part of the background task.
       let operation = RefreshAppContentsOperation()
       
       // Provide the background task with an expiration handler that cancels the operation.
       task.expirationHandler = {
          operation.cancel()
       }


       // Inform the system that the background task is complete
       // when the operation completes.
       operation.completionBlock = {
          task.setTaskCompleted(success: !operation.isCancelled)
       }


       // Start the operation.
       //operationQueue.addOperation(operation)
     }
    
    
    
    private func scheduleAppRefresh() {
       let request = BGAppRefreshTaskRequest(identifier: "app.BackgroundTask")
       // Fetch no earlier than 15 minutes from now.
       request.earliestBeginDate = Date(timeIntervalSinceNow: 60)
        
        print(request)
            
       do {
          try BGTaskScheduler.shared.submit(request)
       } catch {
          print("Could not schedule app refresh: \(error)")
       }
    }

}


final class RefreshAppContentsOperation: Operation, @unchecked Sendable {
    
}

*** Assertion failure in -[BGTaskScheduler _unsafe_registerForTaskWithIdentifier:usingQueue:launchHandler:], BGTaskScheduler.m:225 *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'All launch handlers must be registered before application finishes launching' *** First throw call stack: (0x18b5d321c 0x188a6dabc 0x18a8d1670 0x224da17f0 0x224da15f0 0x102fee124 0x102fee24d 0x102fefe1d 0x102feff79 0x19709d241) libc++abi: terminating due to uncaught exception of type NSException *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'All launch handlers must be registered before application finishes launching' *** First throw call stack: (0x18b5d321c 0x188a6dabc 0x18a8d1670 0x224da17f0 0x224da15f0 0x102fee124 0x102fee24d 0x102fefe1d 0x102feff79 0x19709d241) terminating due to uncaught exception of type NSException

Is this configuration proper, and with this configuration is.addOperation necessary?

Let me explain that exception:

In the traditional app lifecycle, that means registering from application(_:didFinishLaunchingWithOptions:). If you’re using the SwiftUI app lifecycle, you can either use UIApplicationDelegateAdaptor or register from your app’s init method.

Share and Enjoy

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

[1] I don’t think you have to register handlers for all your identifiers but in practice there’s little point listing an identifier you’re not going to register it.

The app no longer crashes, but with my current configuration, the task does not run.

I initialize the task scheduler in the App file's init() function, which calls handleAppRefresh(), which calls RefreshAppContentsOperation()At a given time I call scheduleAppRefresh(), which I thought would preform the task at the scheduled time. The function runs and prints this:

<BGAppRefreshTaskRequest: app.Task, earliestBeginDate: 2025-07-01 21:38:43 +0000>

All of these functions referenced are in my post above. I tried implementing a queue by doing this:

let queue = OperationQueue()

let operation = RefreshAppContentsOperation()

queue.addOperation(operation)

How can I resolve this by Queue or .task?

In general, testing background task code in the debugger is tricky because the debugger interferes with the state of your process. Specifically, it stops it being suspended, so you can’t test the relaunch and resume paths that are critical to background execution.

Given that, you need other ways to debug background execution. I generally do this with logging. I’ve written up detailed advice about this in Testing Background Session Code. A bunch of that doesn’t apply here, because you’re not using URLSession, because the general ideas are still useful.

However, the above is only relevant when you’re further along. During the initial bring up of your background code you can take advantage of a special debugging facility within Background Tasks framework. See Starting and Terminating Tasks During Development.

Share and Enjoy

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

I am getting the following error using the breakpoints below. I'm not sure if that indicates anything. It doesn't appear that RefreshAppContentsOperation runs.

App.debug.dylib`partial apply for closure #1 in View.function():
->  0x103aade10 <+0>:  orr    x29, x29, #0x1000000000000000
    0x103aade14 <+4>:  sub    sp, sp, #0x20
    0x103aade18 <+8>:  stp    x29, x30, [sp, #0x10]
    0x103aade1c <+12>: str    x22, [sp, #0x8]
    0x103aade20 <+16>: add    x29, sp, #0x10
    0x103aade24 <+20>: ldr    x9, [x22]
    0x103aade28 <+24>: str    x9, [sp]
    0x103aade2c <+28>: mov    x8, x29
    0x103aade30 <+32>: sub    x8, x8, #0x8
    0x103aade34 <+36>: str    x9, [x8]
    0x103aade38 <+40>: ldr    x0, [x9, #0x18]
    0x103aade3c <+44>: ldr    x8, [x22]
    0x103aade40 <+48>: mov    x10, x29
    0x103aade44 <+52>: sub    x10, x10, #0x8
    0x103aade48 <+56>: str    x8, [x10]
    0x103aade4c <+60>: str    x8, [x9, #0x10]
    0x103aade50 <+64>: bl     0x103ad626c               ; symbol stub for: swift_task_dealloc
    0x103aade54 <+68>: ldr    x8, [sp]
    0x103aade58 <+72>: ldr    x22, [x8, #0x10]
    0x103aade5c <+76>: ldr    x0, [x22, #0x8]
    0x103aade60 <+80>: ldp    x29, x30, [sp, #0x10]
    0x103aade64 <+84>: and    x29, x29, #0xefffffffffffffff
    0x103aade68 <+88>: add    sp, sp, #0x20
    0x103aade6c <+92>: br     x0
 private func handleAppRefresh(task: BGAppRefreshTask) async {
        Task {
            let queue = OperationQueue()
            
            scheduleAppRefresh() // breakpoint
             
             print("test")  // breakpoint

            // Create an operation that performs the main part of the background task.
            let operation = RefreshAppContentsOperation() // breakpoint
            
            // Provide the background task with an expiration handler that cancels the operation.
            task.expirationHandler = {
               operation.cancel()
            }


            // Inform the system that the background task is complete
            // when the operation completes.
            operation.completionBlock = {
               task.setTaskCompleted(success: !operation.isCancelled)
            }


            // Start the operation.
            queue.addOperation(operation) // breakpoint
          }
         
        }
       
       // Schedule a new refresh task.
       
    
    
    func scheduleAppRefresh() {
       let request = BGAppRefreshTaskRequest(identifier: "app.BackgroundTask")
       // Fetch no earlier than 15 minutes from now.
       request.earliestBeginDate = Date(timeIntervalSinceNow: 60)
        
        print(request) // breakpoint
            
       do {
          try BGTaskScheduler.shared.submit(request)
       } catch {
          print("Could not schedule app refresh: \(error)")
       }
    }

Just following up as I still don't quite understand.

Does RefreshAppContentsOperation run automatically in the background at the designated time if the task is BGTaskScheduler.shared.register() and BGTaskScheduler.shared.submit() ran, or is that why there is operationQueue? If I were to not use operationQueue what would be the alternative?

class BackgroundTasksController: ObservableObject {
    func scheduleTask() {
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "app.Task", using: nil) { task in
            Task {
                await self.handleAppRefresh(task: task as! BGAppRefreshTask)
            }
        }
    }
    
    private func handleAppRefresh(task: BGAppRefreshTask) async {
        Task {
            scheduleAppRefresh()
            
            
            RefreshAppContentsOperation()
        }
    }
    
    func scheduleAppRefresh() {
       let request = BGAppRefreshTaskRequest(identifier: "app.Task")
       request.earliestBeginDate = Date(timeIntervalSinceNow: 60)
        
       do {
          try BGTaskScheduler.shared.submit(request)
       } catch {
          print("Could not schedule app refresh: \(error)")
       }
    }

}


final class RefreshAppContentsOperation: Operation, @unchecked Sendable {
    
}
Background App Refresh
 
 
Q