When I was building my app, Peek, I needed to fetch sales data from App Store Connect every day at a specific time. After doing some quick research, I thought I could just use BGAppRefreshTask because fetching and saving the data into SwiftData only takes about a second, and you can set the earliestBeginDate when creating a BGAppRefreshTaskRequest.

Since I’m using SwiftUI, I attached a .backgroundTask to my WindowGroup like this:

WindowGroup {
  ...
}
.backgroundTask(.appRefresh("com.mertbulan.Peek.backgroundFetch")) {
  fetchSalesData()

  // reschedule a new background task after executing the current one
  scheduleAppRefresh()
}
.onChange(of: phase) { _, newPhase in
    if newPhase == .background {
      scheduleAppRefresh()
    }
}

func scheduleAppRefresh() {
  let request = BGAppRefreshTaskRequest(identifier: "com.mertbulan.Peek.backgroundFetch")
  request.earliestBeginDate = earliestBeginDate // the sales data availability date
  try? BGTaskScheduler.shared.submit(request)
}

To be honest, I was surprised at how easy it was to implement a background task, since this was my first time using one.

After implementing it, I tested my app, and to my surprise, it worked perfectly. The app fetched the sales data exactly when I set the background task. Even if I didn’t open the app for days, it kept fetching the data in the background. Everything worked great until I released the app.

Even though I tested the app with TestFlight without issues, the released version behaved differently when it came to background tasks. After releasing my app, I noticed that the background task wasn’t running, which meant the data stayed outdated.

I checked the Apple Forums for answers and found this post from an Apple Engineer. It says:

Folks assume that app refresh will provide regular background execution time. That’s not the case. The system applies a range of heuristics to decide which apps get app refresh time and when. This is a complex issue, one that I’m not going to try to summarise here, but the take-home message is that, if you expect that the app refresh mechanism will grant you background execution time, say, every 15 minutes, you’ll be disappointed. In fact, there are common scenarios where it won’t grant you any background execution time at all!

So, even though Apple’s article on Choosing Background Strategies for Your App says:

Your app may require short bursts of background time to perform content refresh or other work; for example, your app may fetch content from the server periodically, or regularly update its internal state.

You might think that creating a background task will work like I did. They also mention:

The system decides the best time to launch your background task, and provides your app up to 30 seconds of background runtime.

But they don’t mention that the system could decide not to run your background task at all. Yes, you can set the earliestBeginDate, but that doesn’t mean the task will run at that time, or even after some time.

The issue here is that Apple doesn’t guarantee whether your background task will run. They don’t directly tell you this in the documentation, only in a forum post. So, you should never rely on BGAppRefreshTaskRequest for your business logic. It’s better to think of background app refresh as an addition to your business logic to keep data updated.

For my case, the right way to fetch data at a specific time every day would be to use background notifications. This means sending a notification from your server to the Apple Push Notification Service (APNs) to trigger the background task. The notification isn’t visible to users. The downside is that you need a backend system to send notifications via APNs or a 3rd party service like Firebase.

Since I avoid using third-party services and backends for my apps, using background notifications wasn’t an option for me. This is frustrating because managing notifications with your own backend requires a lot of work. And using a third-party service means they could collect data about your app. I’d expect Apple to provide a better solution for this, like letting you schedule background notifications in App Store Connect and having Apple send them directly to the app.

In the end, I removed the background task from my app for now. Since the system doesn’t guarantee when the task will run—or even if it will run at all—I’m now fetching the sales data whenever the user opens the app. It’s not ideal if you only use widgets, but I’ll try to come up with a solution for that hopefully.


Older post

How to get subscription notifications on iPhone without RevenueCat

Learn how to set up subscription notifications on your iPhone using CloudKit and Cloudflare for free.