I have integrated interactive local notifications. I have created a chatbot using OpenAI. I want to interact with chatbot through interactive local notifications when the app is in the background. When the user responds to an interactive local notification I receive the user response with the below function.
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
if response.actionIdentifier == "replyAction" {
if let textResponse = response as? UNTextInputNotificationResponse {
userQueryText = textResponse.userText
fetchGPTChatforResponse(prompt: userQueryText ?? "")
}
}
completionHandler()
}
After receiving the user response from the interactive notification, I then call the below function:
func fetchGPTChatforResponse(prompt: String) {
Task {
do {
let gptText = try await APIService().sendPromptToGPT(prompt: prompt)
await MainActor.run {
sendInteractiveNotification(message: gptText)
}
} catch {
print("Errrror")
}
}
}
Since my app is in the background, I get following error:
nw_write_request_report [C2] Send failed with error "Socket is not connected"
and
nw_read_request_report [C1] Receive failed with error "Software caused connection abort" sendPromptToGPT
and no local notification is triggred. I have also Implemented the background modes feature like this.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Request notification permission
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
if granted {
print("Notification permission granted")
} else if let error = error {
print("Notification permission denied: \(error.localizedDescription)")
}
}
UNUserNotificationCenter.current().delegate = self
let customCategory = createNotificationCategory()
UNUserNotificationCenter.current().setNotificationCategories([customCategory])
IQKeyboardManager.shared.enable = true
registerBackgroundModes()
return true
}
func registerBackgroundModes() {
print("registerBackgroundModes")
let appRefreshTaskId = "com.xyz.iOSApp.apprefresh"
let appProcessingTaskId = "com.xyz.iOSApp.fetch"
BGTaskScheduler.shared.register(forTaskWithIdentifier: appRefreshTaskId, using: nil) { task in
self.fetchGPTChatforResponse(prompt: self.userQueryText ?? "")
task.setTaskCompleted(success: true)
self.scheduleAppRefresh()
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: appProcessingTaskId, using: nil) { task in
//Logger.shared.info("[BGTASK] Perform bg processing \(appProcessingTaskId)")
self.fetchGPTChatforResponse(prompt: self.userQueryText ?? "")
task.setTaskCompleted(success: true)
self.scheduleBackgroundProcessing()
}
}
func scheduleAppRefresh() {
print("scheduleAppRefresh")
let request = BGAppRefreshTaskRequest(identifier: "com.quilr.iOSApp.apprefresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh task \(error.localizedDescription)")
}
}
func scheduleBackgroundProcessing() {
print("scheduleBackgroundProcessing")
let request = BGProcessingTaskRequest(identifier: "com.quilr.iOSApp.fetch")
request.requiresNetworkConnectivity = true // Need to true if your task need to network process. Defaults to false.
request.requiresExternalPower = true // Need to true if your task requires a device connected to power source. Defaults to false.
request.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60) // Process after 5 minutes.
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule image fetch: (error)")
}
}
And in SceneDelegate.swift I call both the background refresh and background processing functions like this:
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
(UIApplication.shared.delegate as? AppDelegate)?.scheduleAppRefresh()
(UIApplication.shared.delegate as? AppDelegate)?.scheduleBackgroundProcessing()
// Save changes in the application's managed object context when the application transitions to the background.
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
I also enabled background mode by adding the capabilities. But I'm still not getting the local notification when the app is in the background.
You are calling the completionHandler
that is supplied to the delegate method before you have finished your work. This results in iOS suspending your app, which will tear down the network connections.
You need to call the completion handler once fetchGPTChatforResponse
has done its work. You can re-structure your code by moving the Task
into the delegate method and using defer
to ensure that completionHandler
is called once everything is done.
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
Task {
defer {
completionHandler()
}
if response.actionIdentifier == "replyAction",
let textResponse = response as? UNTextInputNotificationResponse {
userQueryText = textResponse.userText
do {
try await fetchGPTChatforResponse(prompt:userQueryText)
}
catch {
print(error)
}
}
}
}
func fetchGPTChatforResponse(prompt: String) async throws {
let gptText = try await APIService().sendPromptToGPT(prompt: prompt)
await MainActor.run {
sendInteractiveNotification(message: gptText)
}
}