swiftnotificationsbackground-processopenai-apiios16

Not receiving the local notification when app is in background


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.


Solution

  • 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)
        }
    }