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

Swift开发:Apple Watch本地通知随机内容失效问题

问题描述

Apple Watch应用中,本地通知功能、通知小时间隔设置以及保存按钮均正常工作,但随机消息展示功能异常:调用randomText()函数选中一条内容后,每次到达设置的间隔时间都会重复推送该内容。

原代码文件

主视图代码

import SwiftUI
import UserNotifications

struct Nottie2: View {
    @AppStorage("notificationInterval") var notificationInterval: Int = 1
    @AppStorage("isSnoozed") var isSnoozed: Bool = false
    @AppStorage("isNotificationsEnabled") var isNotificationsEnabled: Bool = false
    @State private var borderColor = Color.orange
    @State private var buttonText = "Save"

    var body: some View {
        VStack {
            Toggle(isOn: $isNotificationsEnabled) {
                Text(isNotificationsEnabled ? "Turn off" : "Turn on")
            }
            .padding()
            .onChange(of: isNotificationsEnabled) { enabled in
                if enabled {
                    requestPermission()
                } else {
                    disableNotification()
                }
            }
        
            if isNotificationsEnabled {
                Picker("Notification Interval", selection: $notificationInterval) {
                    ForEach(1...6, id: \.self) { interval in
                        Text("\(interval) hour\(interval > 1 ? "s" : "")")
                    }
                }.frame(height: 60)
                .padding()
            
                Button(action: {
                    enableNotification()
                    buttonText = "Saving"
                    borderColor = .green
                
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                        buttonText = "Saved"
                        borderColor = .green
                    }
                
                    DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
                        buttonText = "Save"
                        borderColor = .orange
                    }
                }) {
                    Text(buttonText)
                }
                .foregroundColor(.white)
                .padding(1)
                .frame(width: 75)
                .padding(7)
                .overlay(
                    RoundedRectangle(cornerRadius: 25)
                        .stroke(borderColor, lineWidth: 2))
                .buttonStyle(.plain)
            }
        }
        .onAppear() {
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
                if granted {
                    print("Permission granted")
                } else {
                    print("Permission denied")
                    isNotificationsEnabled = false
                }
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: WKExtension.applicationDidBecomeActiveNotification)) { _ in
            if isSnoozed {
                enableNotification(snooze: true)
            }
        }
    }

请求通知权限函数

private func requestPermission() {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
        if granted {
            print("Permission granted")
            enableNotification()
        } else {
            print("Permission denied")
            isNotificationsEnabled = false
        }
    }
}

通知调度函数

private func enableNotification(snooze: Bool = false) {
    let content = UNMutableNotificationContent()
    content.body = randomText()
    content.sound = UNNotificationSound.default

    var trigger: UNNotificationTrigger
    if snooze {
        trigger = UNTimeIntervalNotificationTrigger(timeInterval: 540, repeats: false)
    } else {
        trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(notificationInterval * 3600), repeats: true)
    }

    let request = UNNotificationRequest(identifier: "notification", content: content, trigger: trigger)

    UNUserNotificationCenter.current().add(request)
}

随机文本生成函数

func randomText() -> String {
    let words = ["Place", "Cat", "House"]
    return words[Int(arc4random_uniform(UInt32(words.count)))]
}

其他通知操作函数

private func disableNotification() {
    UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ["notification"])
}

private func snoozeNotification() {
    isSnoozed = true
    UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ["notification"])
    enableNotification(snooze: true)
}

private func dismissNotification() {
    isSnoozed = false
    UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ["notification"])
}

private func showNotificationActions() {
    let snoozeAction = UNNotificationAction(identifier: "snooze", title: "Snooze", options: [])
    let dismissAction = UNNotificationAction(identifier: "dismiss", title: "Dismiss", options: [.destructive])
    let category = UNNotificationCategory(identifier: "notificationActions", actions: [snoozeAction, dismissAction], intentIdentifiers: [], options: [])
    UNUserNotificationCenter.current().setNotificationCategories([category])
}
}

struct Nottie2_Previews: PreviewProvider {
    static var previews: some View {
        Nottie2()
    }
}
问题根源

使用重复触发的UNTimeIntervalNotificationTrigger时,通知请求的content是在创建请求时一次性生成的,后续重复触发不会重新调用randomText(),因此每次推送的都是同一条随机内容。另外,当前代码未正确处理通知展示后的重新调度,也没有在通知动作(snooze/dismiss)后正确维护通知队列。

解决方案
  1. 将重复触发改为单次触发,每次通知推送完成后重新生成随机内容并调度下一次通知
  2. 实现通知接收代理,监听通知的展示事件,在通知被展示后自动调度下一次通知
  3. 修正动作处理逻辑,确保snooze/dismiss操作后正确清理旧请求并调度新请求
  4. 提前注册通知类别,在应用启动时注册通知动作类别,确保通知能显示自定义按钮
修改后的完整代码
import SwiftUI
import UserNotifications

// 实现通知中心代理
class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
    var parentView: Nottie2?
    
    // 通知即将展示时调用
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert, .sound])
        // 调度下一次通知(非snooze场景)
        if !parentView?.isSnoozed ?? false {
            parentView?.scheduleNextNotification()
        }
    }
    
    // 用户点击通知动作时调用
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        switch response.actionIdentifier {
        case UNNotificationDefaultActionIdentifier:
            // 用户点击通知本身,调度下一次通知
            parentView?.scheduleNextNotification()
        case "snooze":
            parentView?.snoozeNotification()
        case "dismiss":
            parentView?.dismissNotification()
        default:
            break
        }
        completionHandler()
    }
}

struct Nottie2: View {
    @AppStorage("notificationInterval") var notificationInterval: Int = 1
    @AppStorage("isSnoozed") var isSnoozed: Bool = false
    @AppStorage("isNotificationsEnabled") var isNotificationsEnabled: Bool = false
    @State private var borderColor = Color.orange
    @State private var buttonText = "Save"
    private let notificationDelegate = NotificationDelegate()

    var body: some View {
        VStack {
            Toggle(isOn: $isNotificationsEnabled) {
                Text(isNotificationsEnabled ? "关闭通知" : "开启通知")
            }
            .padding()
            .onChange(of: isNotificationsEnabled) { enabled in
                if enabled {
                    requestPermission()
                } else {
                    disableNotification()
                }
            }
        
            if isNotificationsEnabled {
                Picker("通知间隔", selection: $notificationInterval) {
                    ForEach(1...6, id: \.self) { interval in
                        Text("\(interval)小时\(interval > 1 ? "s" : "")")
                    }
                }.frame(height: 60)
                .padding()
            
                Button(action: {
                    disableNotification() // 先清除旧请求
                    scheduleNextNotification()
                    buttonText = "保存中"
                    borderColor = .green
                
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                        buttonText = "已保存"
                        borderColor = .green
                    }
                
                    DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
                        buttonText = "保存"
                        borderColor = .orange
                    }
                }) {
                    Text(buttonText)
                }
                .foregroundColor(.white)
                .padding(1)
                .frame(width: 75)
                .padding(7)
                .overlay(
                    RoundedRectangle(cornerRadius: 25)
                        .stroke(borderColor, lineWidth: 2))
                .buttonStyle(.plain)
            }
        }
        .onAppear() {
            // 设置通知代理
            UNUserNotificationCenter.current().delegate = notificationDelegate
            notificationDelegate.parentView = self
            // 注册通知类别
            showNotificationActions()
            
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
                if granted {
                    print("权限已授予")
                } else {
                    print("权限被拒绝")
                    isNotificationsEnabled = false
                }
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: WKExtension.applicationDidBecomeActiveNotification)) { _ in
            if isSnoozed {
                scheduleNextNotification(snooze: true)
            }
        }
    }
    
    private func requestPermission() {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
            if granted {
                print("权限已授予")
                scheduleNextNotification()
            } else {
                print("权限被拒绝")
                isNotificationsEnabled = false
            }
        }
    }
    
    // 调度下一次通知
    private func scheduleNextNotification(snooze: Bool = false) {
        let content = UNMutableNotificationContent()
        content.body = randomText()
        content.sound = UNNotificationSound.default
        content.categoryIdentifier = "notificationActions" // 指定类别
        
        let timeInterval: TimeInterval
        if snooze {
            timeInterval = 540 // 9分钟
        } else {
            timeInterval = TimeInterval(notificationInterval * 3600)
        }
        
        // 使用单次触发
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: false)
        // 使用唯一标识符,避免覆盖
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
        
        UNUserNotificationCenter.current().add(request)
        isSnoozed = snooze
    }
    
    func randomText() -> String {
        let words = ["Place", "Cat", "House"]
        return words[Int(arc4random_uniform(UInt32(words.count)))]
    }
    
    private func disableNotification() {
        UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
    }
    
    private func snoozeNotification() {
        disableNotification()
        scheduleNextNotification(snooze: true)
    }
    
    private func dismissNotification() {
        disableNotification()
        isSnoozed = false
        if isNotificationsEnabled {
            scheduleNextNotification()
        }
    }
    
    private func showNotificationActions() {
        let snoozeAction = UNNotificationAction(identifier: "snooze", title: "稍后提醒", options: [])
        let dismissAction = UNNotificationAction(identifier: "dismiss", title: "取消", options: [.destructive])
        let category = UNNotificationCategory(identifier: "notificationActions", actions: [snoozeAction, dismissAction], intentIdentifiers: [], options: [])
        UNUserNotificationCenter.current().setNotificationCategories([category])
    }
}

struct Nottie2_Previews: PreviewProvider {
    static var previews: some View {
        Nottie2()
    }
}
关键修改点
  • 新增NotificationDelegate类实现通知代理,监听通知展示和动作点击事件
  • 将重复触发改为单次触发,每次通知展示后自动调度下一次通知
  • 使用UUID().uuidString作为通知请求标识符,避免覆盖旧请求
  • 在保存按钮点击时先清除所有旧请求,再调度新通知
  • 修正dismiss逻辑,取消后自动调度下一次通知(如果通知仍处于开启状态)
  • 提前在onAppear中注册通知类别和设置代理

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 10:10:26