Swift中如何按条件调度不同的重复通知?
问题:调度带条件检查的重复通知时API请求未执行
调度需执行条件检查的重复通知时遇到问题:原本使用Timer触发通知,计划在发送前通过API请求执行条件检查,但API日志未收到任何请求。已确认API可用、NotificationConfig对象正常,暂不考虑后台任务立即结束及重调度问题,相关Swift代码如下:
let notifications: [NotificationConfig] = [...] // notification objects here func buildMap() -> [Int: [NotificationConfig]] { var times = [Int: [NotificationConfig]]() for notif in self.notifications { for time in notif.times { if let value = times[time] { times[time]!.append(notif) } else { times[time] = [notif] } } } return times } func makeCheck(notification: NotificationConfig) -> () async -> Bool { return { print("Called function already") return await self.checkCondition(value: notification.type, hours: notification.hours) } } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { var task: UIBackgroundTaskIdentifier = .invalid task = UIApplication.shared.beginBackgroundTask(withName: "BackgroundTask") { UIApplication.shared.endBackgroundTask(task) } UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { print("Notification authorization granted") let times: [Int: [NotificationConfig]] = self.buildMap() let calendar = Calendar.current let timeZone = TimeZone.current for (desiredHour, notifs) in times { for (i, notif) in notifs.enumerated() { let currentTime = Date() var dateComponents = calendar.dateComponents(in: timeZone, from: currentTime) dateComponents.hour = desiredHour dateComponents.minute = 0 dateComponents.second = 0 guard var desiredTime = calendar.date(from: dateComponents) else { continue } if desiredTime.compare(currentTime) == .orderedAscending { desiredTime = calendar.date(byAdding: .day, value: 1, to: desiredTime)! } let timeDifference = desiredTime.timeIntervalSince(currentTime) let shouldSendNotification = self.makeCheck(notification: notif) Timer.scheduledTimer(withTimeInterval: timeDifference, repeats: true) { _ in Task { print("Starting timed function") if await shouldSendNotification() { print("Sending notificaiton") self.sendNotification(notif: notif, hour: desiredHour, minute: i) } } } print("Timer Interrupt scheduled in \(timeDifference) seconds for \(notif.id) at \(desiredHour)") } } UIApplication.shared.endBackgroundTask(task) print("background task ended") } else { print("Notification authorization denied") } } return true } func sendNotification(notif: NotificationConfig, hour: Int, minute: Int?) { let content = UNMutableNotificationContent() content.title = notif.title content.body = notif.body content.sound = UNNotificationSound.default var dateComponents = DateComponents() dateComponents.hour = hour dateComponents.minute = minute ?? 0 let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false) let identifier = "notification_\(hour)_\(minute ?? 0)" let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { print("Error scheduling notification for hour \(hour): \(error)") } else { print("Notification scheduled successfully for hour \(hour)") } } } func checkCondition(value: NotificationType, hours: Int) async -> Bool { var urlString = MY_URL + "/Notification" urlString += "?value="+value.rawValue+"&hours="+String(hours) guard let url = URL(string: urlString) else { print("Error: Something wrong with url.") return false } var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10) urlRequest.allHTTPHeaderFields = [ "Token": API_KEY ] urlRequest.httpMethod = "GET" do { let (data, response) = try await URLSession.shared.data(for: urlRequest) guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { print("Notification response not 200") return false } return try JSONDecoder().decode(Bool.self, from: data) } catch { print("Error: something went wrong calling api", error) return false } }
解决方案
核心问题分析
原代码无效的根本原因:
- Timer依赖App的RunLoop,当App进入后台后RunLoop会被挂起,Timer无法触发,导致后续的条件检查逻辑完全不会执行。
- 用Timer二次调度本地通知属于冗余操作,本地通知框架本身支持重复触发,无需Timer介入。
方案一:使用UNNotificationServiceExtension(官方推荐)
这是iOS官方提供的在通知展示前执行自定义逻辑的方案,能确保通知触发时执行API检查:
添加Notification Service Extension
在Xcode中选择File > New > Target > Notification Service Extension,按照向导完成添加。在Extension中实现条件检查逻辑
替换Extension的默认代码,加入API检查逻辑:
class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) guard let content = bestAttemptContent, let typeRaw = content.userInfo["notificationType"] as? String, let hours = content.userInfo["hours"] as? Int, let notificationType = NotificationType(rawValue: typeRaw) else { contentHandler(request.content) return } // 执行API条件检查 Task { let shouldShow = await checkCondition(value: notificationType, hours: hours) if shouldShow { contentHandler(content) } else { // 不展示通知,返回空内容 contentHandler(UNNotificationContent()) } } } // 复制原checkCondition方法,注意Extension的网络权限配置 func checkCondition(value: NotificationType, hours: Int) async -> Bool { var urlString = MY_URL + "/Notification" urlString += "?value="+value.rawValue+"&hours="+String(hours) guard let url = URL(string: urlString) else { print("Error: Something wrong with url.") return false } var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10) urlRequest.allHTTPHeaderFields = [ "Token": API_KEY ] urlRequest.httpMethod = "GET" do { let (data, response) = try await URLSession.shared.data(for: urlRequest) guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { print("Notification response not 200") return false } return try JSONDecoder().decode(Bool.self, from: data) } catch { print("Error: something went wrong calling api", error) return false } } override func serviceExtensionTimeWillExpire() { // 超时 fallback,直接返回原通知内容 if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } }
- 修改主App的通知调度逻辑
在sendNotification方法中,将条件检查所需参数放入通知的userInfo,并设置重复触发:
func sendNotification(notif: NotificationConfig, hour: Int, minute: Int?) { let content = UNMutableNotificationContent() content.title = notif.title content.body = notif.body content.sound = UNNotificationSound.default // 传递参数给Extension content.userInfo = [ "notificationType": notif.type.rawValue, "hours": notif.hours ] var dateComponents = DateComponents() dateComponents.hour = hour dateComponents.minute = minute ?? 0 // 设置repeats为true实现每日重复 let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) // 使用唯一标识符避免重复调度 let identifier = "notification_\(notif.id)_\(hour)_\(minute ?? 0)" let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { print("Error scheduling notification for hour \(hour): \(error)") } else { print("Notification scheduled successfully for hour \(hour)") } } }
- 移除原Timer逻辑
删除application(_:didFinishLaunchingWithOptions:)中的Timer相关代码,简化为:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { print("Notification authorization granted") let times = self.buildMap() let calendar = Calendar.current let timeZone = TimeZone.current for (desiredHour, notifs) in times { for (i, notif) in notifs.enumerated() { self.sendNotification(notif: notif, hour: desiredHour, minute: i) } } } else { print("Notification authorization denied") } } return true }
方案二:后台刷新任务+通知调度
如果不想使用Extension,可以通过后台刷新任务定期唤醒App,执行条件检查后调度通知:
开启Background Fetch权限
在项目的Signing & Capabilities中添加Background Modes,勾选Background fetch。实现后台刷新逻辑
在AppDelegate中添加以下方法:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // 遍历所有通知配置,执行条件检查并调度通知 Task { var hasNewData = false for notif in notifications { for desiredHour in notif.times { let shouldSend = await checkCondition(value: notif.type, hours: notif.hours) if shouldSend { sendNotification(notif: notif, hour: desiredHour, minute: 0) hasNewData = true } } } completionHandler(hasNewData ? .newData : .noData) } }
- 初始化后台刷新请求
在application(_:didFinishLaunchingWithOptions:)中请求后台刷新:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { print("Notification authorization granted") // 请求系统定期唤醒App执行后台刷新 UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) } else { print("Notification authorization denied") } } return true }
内容的提问来源于stack exchange,提问作者abhibabi
相关产品推荐
相关产品推荐

