本地通知(Local Notifications)学习笔记

本地通知(Local Notifications)学习笔记

整理自 swiftinsg.org 的 Local Notifications 教程,用自己的话重新梳理 + 补充示例代码,方便复习。

一、整体思路

要给用户发一条本地通知,需要三样东西:

  1. 权限(Permission)------先问用户愿不愿意收通知
  2. 触发器(Trigger)------什么时候发?(多久后 / 某个具体日期时间 / 进出某个地理位置)
  3. 通知内容(Content)------通知里要显示什么、用户能做什么操作

开始之前先在文件顶部导入框架:

swift 复制代码
import UserNotifications

⚠️ 有一点要注意:只有当 App 不在前台运行时,本地通知才会真正弹出来;App 开着的时候是不会跳通知横幅的(除非自己在代理方法里手动允许,见后文)。


二、请求权限

权限弹窗就是我们常见的"「XX」想要发送通知"那个系统提示框。

2.1 可选的权限项

请求权限时要传入一个 options 数组,告诉系统这个 App 需要用到通知的哪些能力:

Option 作用
.badge 允许更新 App 图标右上角的数字角标
.sound 允许通知到达时播放声音
.alert 允许弹出横幅/提醒
.carPlay 允许在 CarPlay 场景下展示通知
.criticalAlert 允许"重要警报"播放声音(即使设备静音)
.providesAppNotificationSettings 在系统通知设置里给这个 App 加一个跳转应用内设置的按钮
.provisional 允许"试用式"通知(见下方说明)

2.2 发起请求

swift 复制代码
let center = UNUserNotificationCenter.current()

Task {
    do {
        try await center.requestAuthorization(options: [.alert, .sound, .badge])
    } catch {
        // 处理错误
        print("请求权限失败:\(error)")
    }
}

实际项目中通常配合一个按钮来触发:

swift 复制代码
Button("开启提醒") {
    Task {
        try? await center.requestAuthorization(options: [.alert, .sound, .badge])
    }
}

2.3 "试用式"权限(Provisional)

加上 .provisional 选项后,用户不会看到权限弹窗,App 会被自动授权,但发出的通知一开始是"安静"的------只会悄悄出现在通知中心历史记录里,不会弹横幅、不会响、也不会更新角标。

swift 复制代码
try await center.requestAuthorization(options: [.alert, .sound, .badge, .provisional])

用户之后会在通知里看到"保留"或"关闭"两个选项:

  • 保留:可以进一步选择"立即送达"或者"按计划摘要送达"
  • 关闭:系统会二次确认,确认后这个 App 就不能再发通知了

因为状态随时可能变化,所以正式发通知前最好都检查一下当前的授权状态。

2.4 检查权限状态

swift 复制代码
let settings = await center.notificationSettings()

guard settings.authorizationStatus == .authorized
        || settings.authorizationStatus == .provisional else {
    return
}

在界面上常见的用法是:根据授权状态决定要不要显示"设置提醒"按钮。

swift 复制代码
struct ReminderView: View {

    let center = UNUserNotificationCenter.current()
    @State private var authorizationStatus: UNAuthorizationStatus?

    var body: some View {
        VStack {
            if authorizationStatus == .authorized || authorizationStatus == .provisional {
                Button("设置提醒") {
                    // 安排通知
                }
            }
        }
        .task {
            let settings = await center.notificationSettings()
            authorizationStatus = settings.authorizationStatus
        }
    }
}

三、三种触发器(Trigger)

3.1 时间间隔触发:UNTimeIntervalNotificationTrigger

多少秒之后触发一次。repeats: true 的话会每隔这个时间间隔重复触发一次。

swift 复制代码
Button("5秒后提醒我喝水") {
    let content = UNMutableNotificationContent()
    content.title = "喝水提醒"
    content.body = "该喝水啦,别忘了补充水分!"
    content.sound = .default

    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)

    center.add(UNNotificationRequest(
        identifier: UUID().uuidString,
        content: content,
        trigger: trigger
    ))
}

3.2 日历/日期触发:UNCalendarNotificationTrigger

传入 DateComponents,只要"当前时间"和你指定的字段匹配上了,就会触发。repeats: true 表示每次匹配上都会触发一次(适合做"每天/每年固定时间提醒")。

每天下午 1 点提醒一次:

swift 复制代码
Button("每天下午1点提醒我开会") {
    let content = UNMutableNotificationContent()
    content.title = "开会提醒"
    content.body = "下午1点的会议要开始了"
    content.sound = .default

    var date = DateComponents()
    date.hour = 13
    date.minute = 0

    let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)

    center.add(UNNotificationRequest(
        identifier: UUID().uuidString,
        content: content,
        trigger: trigger
    ))
}

每年元旦零点提醒一次(指定 month + day,不指定 year 就会每年触发):

swift 复制代码
let content = UNMutableNotificationContent()
content.title = "新年快乐!"
content.body = "新的一年开始啦"
content.sound = .default

var date = DateComponents()
date.hour = 0
date.minute = 0
date.month = 1
date.day = 1

let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)

center.add(UNNotificationRequest(
    identifier: "NewYearReminder",
    content: content,
    trigger: trigger
))

💡 小结:想在某个具体日期 触发,就把 year/month/day/hour/minute 都填上,并把 repeats 设为 false,只会触发那一次。

3.3 地理位置触发:UNLocationNotificationTrigger

进入/离开某个地理范围时触发,需要先导入定位框架:

swift 复制代码
import CoreLocation

需要提供:经纬度、触发半径(米)、进入时触发还是离开时触发(或都触发)。

swift 复制代码
Button("靠近健身房时提醒我") {
    let content = UNMutableNotificationContent()
    content.title = "该锻炼啦"
    content.body = "你已经到健身房附近了"
    content.sound = .default

    let location = CLLocationCoordinate2D(latitude: 1.351082, longitude: 103.842552)

    // 距离这个坐标 2 公里以内会触发
    let region = CLCircularRegion(center: location, radius: 2000.0, identifier: "Gym")
    region.notifyOnEntry = true
    region.notifyOnExit = false

    let trigger = UNLocationNotificationTrigger(region: region, repeats: true)

    center.add(UNNotificationRequest(
        identifier: "GymReminder",
        content: content,
        trigger: trigger
    ))
}

四、自定义通知内容

通知内容是用 UNMutableNotificationContent 来构建的:

swift 复制代码
let content = UNMutableNotificationContent()

4.1 标题、副标题、正文

swift 复制代码
content.title = "待办提醒"
content.subtitle = "今天还有任务没完成"
content.body = "记得在睡前整理一下今天的工作总结哦"

4.2 声音

系统自带几种声音可选:

选项 说明
.default 默认通知音
.defaultCritical 重要警报默认声音
.defaultRingtone 默认铃声
.defaultCriticalSound(withAudioVolume:) 重要警报声音,可指定音量
swift 复制代码
content.sound = .default

自定义音频文件需要满足:

  • 时长不能超过 30 秒(超过的话系统会自动改用默认提示音)
  • 格式必须是:Linear PCM、MA4 (IMA/ADPCM)、µLaw 或 aLaw 之一(可以用命令行工具 afconvert 转换格式)
  • 文件要打包进 App 的 Bundle 里
swift 复制代码
let soundName = UNNotificationSoundName("CustomSound")
content.sound = UNNotificationSound(named: soundName)

其他变体:.criticalSoundNamed(_:)(重要警报自定义声音)、.criticalSoundNamed(_:withAudioVolume:)(可指定音量)、.ringtoneSoundNamed(_:)(自定义铃声)。也可以直接用 /Library/Sounds 里的系统音效。

4.3 附件

可以给通知加图片/音频/视频附件,各自有大小限制:

  • 音频 (限 5MB):.aiff.wav.mp3.m4a
  • 图片 (限 10MB):.jpeg.gif.png
  • 视频 (限 50MB):.mpg/.mpeg.m2v.mp4/.m4v.avi
swift 复制代码
let imageURL = Bundle.main.url(forResource: "Reminder", withExtension: "png")!

content.attachments = [
    try! UNNotificationAttachment(identifier: "ReminderImage", url: imageURL)
]

示例中用了 try! 偷懒,实际项目里记得好好处理错误,不要直接强制解包。

4.4 分组(threadIdentifier)

iPhone / iPad 上默认会把通知折叠分组。想让不同类型的通知分开归类,可以给每一类设置自己的 threadIdentifier

swift 复制代码
content.threadIdentifier = "TodoReminders"

4.5 中断级别(Interruption Level)

控制通知"打扰用户的程度",一共 4 档:

级别 行为
.active(默认) 立即送达,点亮屏幕,可以响铃
.critical 立即送达,点亮屏幕,即使设备静音也会响铃
.passive 安静地加入通知列表,不点亮屏幕,不响铃
.timeSensitive 立即送达,点亮屏幕,可响铃,并且能突破"勿扰模式"等系统通知管控
swift 复制代码
content.interruptionLevel = .passive

使用 .timeSensitive 需要额外开通权限(Entitlement):

  1. 打开项目最外层的 .xcodeproj
  2. 选择 Signing & Capabilities
  3. 点击「+」或按 ⌘ + Shift + L
  4. 搜索 "Time Sensitive Notifications" 并添加

加完之后才能正常使用:

swift 复制代码
content.interruptionLevel = .timeSensitive

五、进阶:通知动作(Actions)

可以让用户不打开 App、直接在通知上做出选择(比如"已读"、"忽略")。

5.1 定义动作

swift 复制代码
let confirmAction = UNNotificationAction(
    identifier: "ConfirmAction",
    title: "完成",
    options: []
)

let dismissAction = UNNotificationAction(
    identifier: "DismissAction",
    title: "忽略",
    options: [.destructive]
)

可选的 options:

  • .authenticationRequired:执行该操作前需要先解锁设备
  • .destructive:标记为"破坏性操作",UI 上会高亮显示(比如红色)
  • .foreground:点击后把 App 拉到前台(不要为了单纯打开 App 而滥用这个选项)

5.2 创建通知分类(Category)并注册

swift 复制代码
let category = UNNotificationCategory(
    identifier: "TodoReminderCategory",
    actions: [confirmAction, dismissAction],
    intentIdentifiers: [],
    hiddenPreviewsBodyPlaceholder: "",
    options: .customDismissAction
)

center.setNotificationCategories([category])

5.3 把分类绑定到通知内容上

swift 复制代码
content.categoryIdentifier = "TodoReminderCategory"

5.4 附带自定义数据(userInfo)

可以在通知里塞一些额外数据,等用户点击动作后再取出来用:

swift 复制代码
content.userInfo = [
    "taskName": "整理周报",
    "priority": 1
]

5.5 处理用户点击的动作

需要让 AppDelegate 遵循 UNUserNotificationCenterDelegate,并实现回调方法:

swift 复制代码
import SwiftUI
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        let userInfo = response.notification.request.content.userInfo
        let taskName = userInfo["taskName"] as! String

        switch response.actionIdentifier {
        case "ConfirmAction":
            print("\(taskName) 已完成")
        case "DismissAction":
            print("\(taskName) 已忽略")
        default:
            break
        }

        // 一定要记得调用,否则系统会认为还没处理完
        completionHandler()
    }
}

@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

六、设置角标数字(Badge)

有两种方式可以更新 App 图标右上角的数字:

方式一:通过某条通知的内容设置

swift 复制代码
content.badge = 100

方式二:直接通过通知中心设置(不依赖某条具体通知)

swift 复制代码
center.setBadgeCount(100)

七、速查总结

想做什么 用什么
请求发通知的权限 requestAuthorization(options:)
检查当前权限状态 notificationSettings()
多少秒后触发一次 UNTimeIntervalNotificationTrigger
在某个具体日期/每天/每年固定时间触发 UNCalendarNotificationTrigger + DateComponents
进入/离开某个地点时触发 UNLocationNotificationTrigger + CLCircularRegion
设置标题/副标题/正文/声音 UNMutableNotificationContent
通知带图片/音频/视频 content.attachments
通知折叠分组 content.threadIdentifier
控制打扰程度 content.interruptionLevel
通知上加可点击按钮 UNNotificationAction + UNNotificationCategory
处理用户点击的按钮 UNUserNotificationCenterDelegate
更新角标数字 content.badgecenter.setBadgeCount(_:)

📌 记住核心三件套:权限 → 触发器 → 内容 ,三者备齐,最后用 center.add(UNNotificationRequest(...)) 提交即可。

相关推荐
森蓝情丶2 小时前
我给 AI 搭了个法庭:一个前端仔的 LangGraph 实战全记录
前端·后端
爱勇宝2 小时前
干了近 8 年,一夜之间被裁:AI 时代,程序员最该害怕的不是 AI
前端·后端·程序员
Pedantic2 小时前
Combine 框架学习笔记
前端
runnerdancer2 小时前
Agent如何加载执行Skill的脚本
前端·agent
yingyima3 小时前
VS Code 正则替换技巧:从凌晨3点的服务器报警开始
前端
默_笙3 小时前
🛬 我让 AI 帮我写了一个打飞机游戏,结果 Canvas 把我整不会了
前端·javascript
梯度不陡3 小时前
AI 到底能不能从零写软件?ProgramBench 和 RepoZero 给出了两种答案
前端·javascript·面试
冬奇Lab3 小时前
每日一个开源项目(第137篇):Penpot - 真正开源的设计协作工具,SVG 原生格式消灭设计-开发鸿沟
前端·开源·设计
nuIl3 小时前
实现一个 Coding Agent(7):Skills
前端·agent·cursor