Background Tasks problem

Hello everyone!

I need to run a function every X time, and after searching all over the web, I realized that the best way for that is using the Background Tasks framework. Problem is, although I manage to successfully register and submit a Task, it never happens.

About the code, I register the tasks when the app is launched:

@main
struct PersonalEnciclopediaApp: App {
  
  init() {
   NotificationManager.shared.requestAuthorization()
   BackgroundTaskManager.shared.register()
  }
  
  var body: some Scene {
    WindowGroup {
      BackgroundTasks()
    }
  }
}

The above code requests notification authorization (which is working fine), and fires the register function:

func register() {
   BGTaskScheduler.shared.register(forTaskWithIdentifier: identifier, using: .main) { task in
     self.handleTask(task)
   }
   scheduleAppRefresh()
  }

The code above registers my task, schedules a new Task with the method scheduleAppRefresh() and, for what I understood, tells the system that the functions that needs to be called when the task starts is handleTask(), which is:

func handleTask(_ task: BGTask) {
   scheduleAppRefresh()

   show(message: "handleTask: \(task.identifier)")

   let request = performRequest { error in
     task.setTaskCompleted(success: error == nil)
   }

   task.expirationHandler = {
     task.setTaskCompleted(success: false)
     request.cancel()
   }
  }

Again, for what I understood, the handleTask method calls the method performRequest, which is:

func performRequest(completion: @escaping (Error?) -> Void) -> URLSessionTask {
   show(message: "starting performRequest")
   let url = URL(string: "https://httpbin.org/get")!
   let task = URLSession.shared.dataTask(with: url) { _, _, error in
     print("finished request")
     completion(error)
   }

   task.resume()

   return task
  }

And just so that this post is complete, the show method's responsability is just showing a notification:

func show(message: String) {
   let content = UNMutableNotificationContent()
   content.title = "AppRefresh task"
   content.body = message
   let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: false)
   let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
   UNUserNotificationCenter.current().add(request) { error in
     if let error {
       print("error \(error.localizedDescription)")
     }
   }
  }

Here is the complete class (just in case its needed):

import Foundation
import BackgroundTasks
import UserNotifications

extension BackgroundTaskManager {
  
  static let shared = BackgroundTaskManager()
  private init() { }
  
  let identifier = "com.hsilvgar.notifications"
  
  func register() {
   BGTaskScheduler.shared.register(forTaskWithIdentifier: identifier, using: .main, launchHandler: handleTask(_:))
   scheduleAppRefresh()
  }
  
  func handleTask(_ task: BGTask) {
   scheduleAppRefresh()

   show(message: "handleTask: \(task.identifier)")

   let request = performRequest { error in
     task.setTaskCompleted(success: error == nil)
   }

   task.expirationHandler = {
     task.setTaskCompleted(success: false)
     request.cancel()
   }
  }
  
  func scheduleAppRefresh() {
   let request = BGAppRefreshTaskRequest(identifier: self.identifier)

   var message = "Scheduled"
   do {
     try BGTaskScheduler.shared.submit(request)
   } catch BGTaskScheduler.Error.notPermitted {
     message = "BGTaskScheduler.shared.submit notPermitted"
   } catch BGTaskScheduler.Error.tooManyPendingTaskRequests {
     message = "BGTaskScheduler.shared.submit tooManyPendingTaskRequests"
   } catch BGTaskScheduler.Error.unavailable {
     message = "BGTaskScheduler.shared.submit unavailable"
   } catch {
     message = "BGTaskScheduler.shared.submit \(error.localizedDescription)"
   }

   show(message: "scheduleAppRefresh: \(message)")
  }
  
  func show(message: String) {
   let content = UNMutableNotificationContent()
   content.title = "AppRefresh task"
   content.body = message
   let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: false)
   let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
   UNUserNotificationCenter.current().add(request) { error in
     if let error {
       print("error \(error.localizedDescription)")
     }
   }
  }
  
  func performRequest(completion: @escaping (Error?) -> Void) -> URLSessionTask {
   show(message: "starting performRequest")
   let url = URL(string: "https://httpbin.org/get")!
   let task = URLSession.shared.dataTask(with: url) { _, _, error in
     print("finished request")
     completion(error)
   }

   task.resume()

   return task
  }
}

So the problem is, I get the "Scheduled" notification, but I never get the notification from within performRequest(), which would tell me the task was processed.

PS: 1- I have already registered "Background Modes" with "Background fetch" and "Background processing" on my App's Signing & Capabilities 2- I have already registered the Identifier on Info.plist (in BGTaskSchedulerPermittedIdentifiers)

Here is my info.plist (if needed)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>BGTaskSchedulerPermittedIdentifiers</key>
	<array>
		<string>com.hsilvgar.notifications</string>
	</array>
  
	<key>UIApplicationExitsOnSuspend</key>
	<false/>
  
	<key>UIBackgroundModes</key>
	<array>
		<string>fetch</string>
		<string>processing</string>
	</array>
</dict>
</plist>

Does anyone know what am I missing, or what is wrong?

Replies

I need to run a function every X time

What is the value for X?

Share and Enjoy

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

Ideally, it would be twice or thrice a day.. 00h, 08h and 16h maybe? I have some notifications that need to be created everyday, depending on some factors. When a user creates a new task or something, it goes to coreData, and then I have a function that looks at coreData and adds a local notification if needed. But I need that to happen even when the app is closed, twice or thrice a day.

When a user creates a new task or something, it goes to coreData, and then I have a function that looks at coreData and adds a local notification if needed. But I need that to happen even when the app is closed

How can the user create a new task and put it into Core Data if the app isn’t running?

Share and Enjoy

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

Sorry, I think I didn't explain it clearly.

Users can add medication reminders. Originally, the idea was to use UNUserNotifications only, but it doesn't work as expected, because we can add a daily notification that repeats, for example, everyday at 3pm. But there are cases in which the user wants to add a medication that needs to be taken on a daily basis, at a certain time, but they will only start taking it at a specific date, on the future. UNUserNotifications has a functionality to repeat a notification everyday at a certain time, but not with a start date.

That's where Core Data takes place. If the medication starts today or tomorrow, I can just create a notification. If its in the future, I can not create a notification. So as a workaround, I add an object with all the information about that notification to Core Data, and I have a function that iterates over all CoreData objects, looking for notifications that should be created today.

And that, my dear friend, is what I need to run everyday, once or twice a day.

So no, the user won't create a new task and put it into Core Data while the app isn't running. The user will add a medication (with the app running), and if it has a future starting date, it will go straight to Core Data, and not to Notifications Center. So everyday I need to run a function that looks at Core Data, searching for notifications to create!

Thanks for the explanation.

You wrote:

UNUserNotifications has a functionality to repeat a notification everyday at a certain time, but not with a start date.

To start, I encourage you to file an enhancement request for that. It’d make everything easier (-: Please post your bug number, just for the record.

As to a workaround, what I’d probably do is something like this:

  1. If you have one of these events in play…

  2. Create a background processing task. Typically this will run each night.

  3. On the night before the event is supposed to start [1], schedule your repeating notification.

  4. But, independent of your background task stuff, schedule a non-repeating notification for the morning of that day. That notification is not “take your meds” but rather “run the app for an important update”. This acts as a fallback if step 3 doesn’t happen. Hopefully the user will run the app and you can then explain that you’ve now scheduled their reminders.

    Make sure to cancel this notification if step 3 actually runs.

Share and Enjoy

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

[1] Note that this doesn’t have to be at night. The Background Tasks framework makes no guarantees about the time. What you’re actually looking for is that the event is less than a day away, so you can schedule your repeating event without it firing at the wrong time.