应用未运行时,选择本地通知操作能否正常执行?
我用本地通知在会话即将开始或结束时提醒用户,启动/结束会话时应用会执行多项任务。现在希望通过通知操作实现无需前台启动应用就能完成会话的启动/结束。
当应用在后台运行时,点击通知操作能正常调用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

