You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

应用未运行时,选择本地通知操作能否正常执行?

问题:应用被强制关闭后,通知操作无法触发didReceive代理方法

我用本地通知在会话即将开始或结束时提醒用户,启动/结束会话时应用会执行多项任务。现在希望通过通知操作实现无需前台启动应用就能完成会话的启动/结束。

当应用在后台运行时,点击通知操作能正常调用UNUserNotificationCenterDelegate的didReceive方法,获取UNNotificationResponse,会话相关任务也能正常执行。但如果从应用切换器强制关闭应用后,点击通知操作,didReceive代理方法似乎没被调用,应用无法执行对应的会话启动/结束任务。

苹果可操作通知的文档说明:

可操作通知允许用户无需启动对应应用即可响应已送达的通知。其他通知仅在通知界面展示信息,用户只能选择启动应用。可操作通知会在通知界面额外显示一个或多个按钮,点击按钮会将所选操作发送给应用,应用在后台处理该操作。

我是不是操作有误,或者对通知操作的工作原理理解错了?


相关代码

AppDelegate.swift

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    let injectionContainer = AppContainer()
    let dbMaintenanceManager = DBMaintenanceManager()
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
        
        requestNotificationAuthorization()
        
        dbMaintenanceManager.performMaintenanceTasks()
        
        let appearance = UINavigationBarAppearance()
        appearance.backgroundColor = Color.primaryBlue
        appearance.titleTextAttributes = [.foregroundColor: UIColor.white]
        
        UINavigationBar.appearance().standardAppearance = appearance
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
        UINavigationBar.appearance().tintColor = .white
      
        let mainVC = injectionContainer.makeMainViewController()

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.overrideUserInterfaceStyle = .light
        window?.makeKeyAndVisible()
        window?.rootViewController = mainVC

        return true
    }
        
    func applicationWillEnterForeground(_ application: UIApplication) {
        dbMaintenanceManager.performMaintenanceTasks()
    }
    
    private func requestNotificationAuthorization() {
        let notificationCenter = UNUserNotificationCenter.current()
        
        notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if let error = error {
                print("Error requesting authorization: \(error)")
                return
            }

            if granted {
                let startAction = UNNotificationAction(identifier: "START_ACTION",
                                                              title: "Start Session",
                                                       options: [])
                
                let endAction = UNNotificationAction(identifier: "END_ACTION",
                                                            title: "End Session",
                                                     options: [])
                
                let startCategory = UNNotificationCategory(identifier: "START_CATEGORY",
                                                                  actions: [startAction],
                                                                  intentIdentifiers: [],
                                                                  options: [])
                
                let endCategory = UNNotificationCategory(identifier: "END_CATEGORY",
                                                                  actions: [endAction],
                                                                  intentIdentifiers: [],
                                                                  options: [])
                
                let notificationCenter = UNUserNotificationCenter.current()
                notificationCenter.setNotificationCategories([startCategory, endCategory])
                notificationCenter.delegate = self
                print("Notification permission authorized")
            } else {
                print("Notification permission denied")
            }
        }
    }
}

// MARK: - UNUserNotificationCenterDelegate

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        NotificationCenter.default.post(name: .didReceivePushNotification, object: notification)
        completionHandler([.banner, .list, .sound])
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        handleNotificationResponse(response)
        completionHandler()
    }
        
    private func handleNotificationResponse(_ response: UNNotificationResponse) {
        let userInfo = response.notification.request.content.userInfo
        guard let id = userInfo["sessionId"] as? String else {
            print("Error reading session id associated with notification")
            return
        }

        switch response.actionIdentifier {
        case "START_ACTION":
            NotificationCenter.default.post(name: .didStartSession, object: nil, userInfo: ["sessionId" : id])
        case "END_ACTION":
            NotificationCenter.default.post(name: .didEndSession, object: nil, userInfo: ["sessionId" : id])
        default:
            break
        }
    }
}

extension Notification.Name {
    static let didReceivePushNotification = Notification.Name("didReceivePushNotification")
    static let didStartSession = Notification.Name("didStartSession")
    static let didEndSession = Notification.Name("didEndSession")
}

NotificationScheduler.swift

class NotificationScheduler {
    
    enum NotificationType {
        case reminder
        case completion
    }
    
    let notificationCenter = UNUserNotificationCenter.current()
    
    func scheduleNotification(for session: Session, type: NotificationType) {
        switch session.type {
        case .now:
            scheduleNowNotification(session: session, type: type)
        case .later:
            scheduleLaterNotification(session: session, type: type)
        case .recurring:
            scheduleRecurringNotification(session: session, type: type)
        case .none:
            break
        }
    }
    
    private func scheduleNowNotification(session: Session, type: NotificationType) {
        if type == .completion { scheduleCompletionNotification(session: session, date: session.endTime) }
    }
    
    private func scheduleLaterNotification(session: Session, type: NotificationType) {
        if type == .reminder { scheduleReminderNotification(session: session, date: session.startTime) }
        if type == .completion { scheduleCompletionNotification(session: session, date: session.endTime) }
    }
    
    private func scheduleRecurringNotification(session: Session, type: NotificationType) {
        for day in session.recurringDays {
            if type == .reminder { scheduleReminderNotification(session: session, date: session.startTime, recurringDay: day) }
            if type == .completion { scheduleCompletionNotification(session: session, date: session.endTime, recurringDay: day) }
        }
    }
    
    private func scheduleReminderNotification(session: Session, date: Date, recurringDay: Weekday? = nil) {
        let content = UNMutableNotificationContent()
        content.title = "Reminder"
        content.body = "Your session \(session.name) is starting."
        content.sound = .default
        content.categoryIdentifier = "START_CATEGORY"
        content.userInfo = ["sessionId" : session._id.stringValue]
        
        let trigger: UNNotificationTrigger
        if let recurringDay = recurringDay {
            trigger = createWeeklyTrigger(date: date, weekday: recurringDay)
        } else {
            trigger = createTrigger(date: date)
        }
        
        let request = UNNotificationRequest(identifier: "\(session._id)-reminder", content: content, trigger: trigger)
        notificationCenter.add(request, withCompletionHandler: nil)
    }
    
    private func scheduleCompletionNotification(session: Session, date: Date, recurringDay: Weekday? = nil) {
        let content = UNMutableNotificationContent()
        content.title = "Session Complete"
        content.body = "Your session \(session.name) is complete."
        content.sound = .default
        content.categoryIdentifier = "END_CATEGORY"
        content.userInfo = ["sessionId" : session._id.stringValue]
        
        let trigger: UNNotificationTrigger
        if let recurringDay = recurringDay {
            trigger = createWeeklyTrigger(date: date, weekday: recurringDay)
        } else {
            trigger = createTrigger(date: date)
        }
        
        let request = UNNotificationRequest(identifier: "\(session._id)-completion", content: content, trigger: trigger)
        notificationCenter.add(request, withCompletionHandler: nil)
    }
    
    private func createTrigger(date: Date) -> UNCalendarNotificationTrigger {
        let calendar = Calendar.current
        let components = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
        return UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
    }
    
    private func createWeeklyTrigger(date: Date, weekday: Weekday) -> UNCalendarNotificationTrigger {
        var components = Calendar.current.dateComponents([.hour, .minute, .second], from: date)
        components.weekday = weekday.rawValue
        return UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
    }
    
    func updateNotification(for session: Session) {
        // Remove existing notifications for this session
        removeNotification(for: session._id)
        
        // Schedule new notifications
        if session.type == .now {
            scheduleNotification(for: session, type: .completion)
        } else {
            scheduleNotification(for: session, type: .reminder)
        }
        
    }
    
    func removeNotification(for sessionId: ObjectId) {
        notificationCenter.removePendingNotificationRequests(withIdentifiers: ["\(sessionId)-reminder"])
    }
}

原因分析与解决方案

核心原因

当应用被**强制关闭(从切换器划走)**后,iOS不会唤醒应用处理普通通知操作——只有标记为.background的通知操作,才具备触发后台唤醒的权限。你的START_ACTION和END_ACTION创建时未设置该选项,导致应用关闭后无法响应。

另外,你在权限请求的异步回调中设置通知分类和代理,可能导致配置不及时,极端场景下代理未被正确绑定。

修复步骤

1. 给通知操作添加.background选项

修改通知操作的创建代码,添加.background选项:

let startAction = UNNotificationAction(identifier: "START_ACTION",
                                      title: "Start Session",
                                      options: [.background])

let endAction = UNNotificationAction(identifier: "END_ACTION",
                                    title: "End Session",
                                    options: [.background])

2. 提前配置通知分类与代理

将通知分类和代理的设置移到application:didFinishLaunchingWithOptions中,确保应用启动时完成初始化:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
    // 先配置通知分类和代理
    setupNotificationCategories()
    UNUserNotificationCenter.current().delegate = self
    
    requestNotificationAuthorization()
    
    // 原有其他代码...
    return true
}

private func setupNotificationCategories() {
    let startAction = UNNotificationAction(identifier: "START_ACTION",
                                          title: "Start Session",
                                          options: [.background])
    
    let endAction = UNNotificationAction(identifier: "END_ACTION",
                                        title: "End Session",
                                        options: [.background])
    
    let startCategory = UNNotificationCategory(identifier: "START_CATEGORY",
                                              actions: [startAction],
                                              intentIdentifiers: [],
                                              options: [])
    
    let endCategory = UNNotificationCategory(identifier: "END_CATEGORY",
                                            actions: [endAction],
                                            intentIdentifiers: [],
                                            options: [])
    
    UNUserNotificationCenter.current().setNotificationCategories([startCategory, endCategory])
}

3. 保证后台任务执行完整性

如果会话启动/结束任务耗时较长,需要启动后台任务来防止系统中断执行:

private func handleNotificationResponse(_ response: UNNotificationResponse) {
    var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
    backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "SessionActionTask") {
        UIApplication.shared.endBackgroundTask(backgroundTaskID)
        backgroundTaskID = .invalid
    }
    
    let userInfo = response.notification.request.content.userInfo
    guard let id = userInfo["sessionId"] as? String else {
        print("Error reading session id associated with notification")
        UIApplication.shared.endBackgroundTask(backgroundTaskID)
        return
    }

    switch response.actionIdentifier {
    case "START_ACTION":
        NotificationCenter.default.post(name: .didStartSession, object: nil, userInfo: ["sessionId" : id])
    case "END_ACTION":
        NotificationCenter.default.post(name: .didEndSession, object: nil, userInfo: ["sessionId" : id])
    default:
        break
    }
    
    UIApplication.shared.endBackgroundTask(backgroundTaskID)
    backgroundTaskID = .invalid
}

额外注意事项

  • 即使添加.background选项,iOS也可能因系统资源紧张拒绝唤醒应用,但常规场景下均可正常工作。
  • 测试时需等待通知自然触发,手动测试的通知可能不具备后台唤醒权限。

内容的提问来源于stack exchange,提问作者Kevin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 21:24:51