本地通知(Local Notifications)学习笔记
整理自 swiftinsg.org 的 Local Notifications 教程,用自己的话重新梳理 + 补充示例代码,方便复习。
一、整体思路
要给用户发一条本地通知,需要三样东西:
- 权限(Permission)------先问用户愿不愿意收通知
- 触发器(Trigger)------什么时候发?(多久后 / 某个具体日期时间 / 进出某个地理位置)
- 通知内容(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):
- 打开项目最外层的
.xcodeproj - 选择 Signing & Capabilities
- 点击「+」或按
⌘ + Shift + L - 搜索 "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.badge 或 center.setBadgeCount(_:) |
📌 记住核心三件套:权限 → 触发器 → 内容 ,三者备齐,最后用
center.add(UNNotificationRequest(...))提交即可。