NiceNotifications 重新构想了 Apple 平台上的本地通知。
它为开发者提供了一种管理通知调度、权限和分组的新方式。
最基本的形式是,它可以帮助以声明式的方式轻松调度本地通知。
在其最先进的形式中,它引入了一种全新的本地通知查看方式,具有类似于 WidgetKit
或 ClockKit
API 的“通知时间线”概念。
作者 & 维护者:@dreymonde
警告! 目前,NiceNotifications 处于早期 beta 阶段。某些 API 可能在版本之间发生变化。预计会出现重大更改。非常欢迎对 API 提出反馈!
import NiceNotifications
LocalNotifications.schedule(permissionStrategy: .askSystemPermissionIfNeeded) {
EveryMonth(forMonths: 12, starting: .thisMonth)
.first(.friday)
.at(hour: 20, minute: 15)
.schedule(title: "First Friday", body: "Oakland let's go!")
}
http://github.com/nicephoton/NiceNotifications.git
// `NotificationContent` is a subclass of `UNNotificationContent`.
// You can also use `UNNotificationContent` directly
let content = NotificationContent(
title: "Test Notification",
body: "This one is for a README",
sound: .default
)
LocalNotifications.schedule(
content: content,
at: Tomorrow().at(hour: 20, minute: 15),
permissionStrategy: .scheduleIfSystemAllowed
)
在大多数情况下,NiceNotifications 会为您处理所有权限相关事宜。因此,您可以随时安排通知,权限策略将负责处理权限。
基本权限策略
askSystemPermissionIfNeeded
- 如果已经授予权限,将继续调度。如果尚未请求权限,它将请求系统权限,然后成功后继续。如果之前被拒绝,则不会继续。scheduleIfSystemAllowed
- 仅当之前已授予权限时才会继续调度。否则,将不执行任何操作。NiceNotifications 使用 DateBuilder 以简单、清晰且易于阅读的方式帮助定义通知触发日期。请参阅 DateBuilder README 获取完整详细信息。
这是一个简短的参考
Today()
.at(hour: 20, minute: 15)
NextWeek()
.weekday(.saturday)
.at(hour: 18, minute: 50)
EveryWeek(forWeeks: 10, starting: .thisWeek)
.weekendStartDay
.at(hour: 9, minute: 00)
EveryDay(forDays: 30, starting: .today)
.at(hour: 19, minute: 15)
ExactlyAt(account.createdAt)
.addingDays(15)
WeekOf(account.createdAt)
.addingWeeks(1)
.lastDay
.at(hour: 10, minute: 00)
EveryMonth(forMonths: 12, starting: .thisMonth)
.lastDay
.at(hour: 23, minute: 50)
NextYear().addingYears(2)
.firstMonth.addingMonths(3) // April (in Gregorian)
.first(.thursday)
ExactDay(year: 2020, month: 10, day: 5)
.at(hour: 10, minute: 15)
ExactYear(year: 2020)
.lastMonth
.lastDay
LocalNotifications.schedule(permissionStrategy: .scheduleIfSystemAllowed) {
Today()
.at(hour: 20, minute: 30)
.schedule(title: "Hello today", sound: .default)
Tomorrow()
.at(hour: 20, minute: 45)
.schedule(title: "Hello tomorrow", sound: .default)
} completion: { result in
if result.isSuccess {
print("Scheduled!")
}
}
警告! iOS 只允许您最多安排 64 个本地通知,其余的将被静默丢弃。(文档)
func randomContent() -> NotificationContent {
return NotificationContent(title: String(Int.random(in: 0...100)))
}
LocalNotifications.schedule(permissionStrategy: .askSystemPermissionIfNeeded) {
EveryDay(forDays: 30, starting: .today)
.at(hour: 20, minute: 30, second: 30)
.schedule(with: randomContent)
}
对于基于日期的循环内容
func content(forTriggerDate date: Date) -> NotificationContent {
// create content based on date
}
LocalNotifications.schedule(permissionStrategy: .askSystemPermissionIfNeeded) {
EveryDay(forDays: 30, starting: .today)
.at(hour: 20, minute: 30, second: 30)
.schedule(with: content(forTriggerDate:))
}
let group = LocalNotifications.schedule(permissionStrategy: .askSystemPermissionIfNeeded) {
EveryDay(forDays: 30, starting: .today)
.at(hour: 15, minute: 30)
.schedule(title: "Hello!")
}
// later:
LocalNotifications.remove(group: group)
LocalNotifications.requestPermission(strategy: .askSystemPermissionIfNeeded) { success in
if success {
print("Allowed")
}
}
LocalNotifications.SystemAuthorization.getCurrent { status in
switch status {
case .allowed:
print("allowed")
case .deniedNow:
print("denied")
case .deniedPreviously:
print("denied and needs to enable in settings")
case .undetermined:
print("not asked yet")
}
if status.isAllowed {
print("can schedule!")
}
}
如果您只想使用 NiceNotifications 的权限部分并自己创建 UNNotificationRequest
实例,请使用 .directSchedule
函数
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 360, repeats: true)
let content = UNMutableNotificationContent()
content.title = "Repeating"
content.body = "Every 6 minutes"
content.sound = .default
let request = UNNotificationRequest(
identifier: "repeating_360",
content: content,
trigger: trigger
)
LocalNotifications.directSchedule(
request: request,
permissionStrategy: .askSystemPermissionIfNeeded
) // completion is optional
NiceNotifications 最强大的功能是通知组内的时间线,它允许您以类似 WidgetKit 的方式描述您的整个本地通知体验。
假设我们有一个应用程序,每天早上从列表中显示不同的名言。 用户还可以禁用/启用某些名言,或添加自己的名言。
为此,我们需要定义一个新类来实现 LocalNotificationsGroup
协议
public protocol LocalNotificationsGroup {
var groupIdentifier: String { get }
func getTimeline(completion: @escaping (NotificationsTimeline) -> ())
}
组不仅允许您在不同的体验之间进行清晰的逻辑分离,而且还允许您在每个组的基础上拥有用户权限(我们稍后会介绍)。
让我们实现我们的 DailyQuoteGroup
final class DailyQuoteGroup: LocalNotificationsGroup {
let groupIdentifier: String = "dailyQuote"
func getTimeline(completion: @escaping (NotificationsTimeline) -> ()) {
let timeline = NotificationsTimeline {
EveryDay(forDays: 50, starting: .today)
.at(hour: 9, minute: 00)
.schedule(title: "Storms make oaks take deeper root.")
}
completion(timeline)
}
}
但这只会让我们在接下来的 50 天内获得 50 个相同的名言。 让我们让它更有趣,每天给用户一个真正的随机名言
final class DailyQuoteGroup: LocalNotificationsGroup {
let groupIdentifier: String = "dailyQuote"
func getTimeline(completion: @escaping (NotificationsTimeline) -> ()) {
let timeline = NotificationsTimeline {
EveryDay(forDays: 50, starting: .today)
.at(hour: 9, minute: 00)
.schedule(with: makeRandomQuoteContent)
}
completion(timeline)
}
private func makeRandomQuoteContent() -> NotificationContent? {
guard let randomQuote = QuoteStore.enabledQuotes.randomElement() else {
return nil
}
return NotificationContent(
title: randomQuote,
body: "Tap here for more daily inspiration"
)
}
}
看起来很棒! 每次调用 makeRandomQuoteContent
时,我们都会得到一个不同的名言,这正是我们想要的。
好的,那么我们现在用它做什么呢?
调度通知组很容易
LocalNotifications.reschedule(
group: DailyQuoteGroup(),
permissionStrategy: .askSystemPermissionIfNeeded
) // completion is optional
为什么称之为“重新调度”? 因为每次我们用同一个组调用这个函数时,整个时间线将被清除并重新创建。
为什么这很有用? 首先,假设用户已禁用其中一个名言的显示。 但它可能已经被安排好了! 没问题:我们将简单地再次调用 reschedule,它将不再显示
QuoteStore.disableQuote(userDisabledQuote)
LocalNotifications.reschedule(
group: DailyQuoteGroup(),
permissionStrategy: .scheduleIfSystemAllowed
)
由于 DailyQuoteGroup
使用 QuoteStore.enabledQuotes
生成随机名言,因此新重新调度的组将不再具有禁用的名言!
其次,您已经注意到,我们只安排了从“今天”开始的 50 天。 这是因为我们无法使用系统循环通知(因为它只允许我们对每个通知使用相同的内容),并且 iOS 只允许我们一次最多 64 个已调度的通知。
所以是的,它将要求我们定期重新调度该组以“重置”这 50 天。 最好的地方之一是 applicationDidFinishLaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
LocalNotifications.reschedule(
group: DailyQuoteGroup(),
permissionStrategy: .scheduleIfSystemAllowed
)
return true
}
或者,您可以安排后台执行任务以定期刷新通知。
权限策略有两个不同的级别
以下是如何创建您自己的自定义权限策略
LocalNotifications.PermissionStrategy(
groupLevel: PermissionStrategy.GroupLevel,
systemLevel: PermissionStrategy.SystemLevel
)
权限策略将始终首先执行组级别策略,如果成功,将继续执行系统级别策略。
将跳过组级别权限检查并直接进入系统级别。 这不会更改现有的组级别权限(如果存在)。
将在组级别启用权限并保存该决定,然后继续进行系统级别检查。
如果用户之前禁用/拒绝了此组权限,则 .allowAutomatically
将覆盖该决定。
将在继续进行系统级别检查之前询问用户的许可,并将保存该决定。 只有在之前未授予许可的情况下才会询问许可,否则将直接继续进行系统级别检查。
AskPermissionMode:
.once
:只会询问一次权限。 如果用户拒绝了此组,则任何后续调用都不会询问权限,也不会安排通知.alwaysIfNotAllowed
:如果尚未授予权限,则始终询问权限PermissionAsker:
此类负责询问组级别权限。 您可以使用 .defaultAlert(on:)
显示预先制作的警报(仅限英语),使用 .alert(on:title:message:noActionTitle:yesActionTitle:)
,或创建您自己的警报
let permissionAsker = LocalNotifications.ApplicationLevelPermissionAsker { (completion) in
// ask permission, then call completion with Result<Bool, Error>
}
仅当之前允许该类别时才会继续进行系统级别检查
仅当允许其他指定的类别时才会继续进行系统级别检查
如果需要,将请求系统通知权限
仅当系统已经允许时才会继续调度通知; 否则将不会继续
对于 UIKit
,NiceNotifications 提供了 NotificationsPermissionSwitch
,这是一个自定义 UIView
,显示并允许修改通知组的组级别权限
let toggle = NotificationsPermissionSwitch(group: DailyQuoteGroup())
toggle.onEnabled = { _ in ... }
toggle.onDisabled = { _ in ... }
toggle.onDeniedBySystem = { _ in /* show "Open Settings" alert to user */ }
这为您节省了很多通常需要自己实现的复杂性。
如果您想在用户尝试启用该类别时显示自己的预先权限,则可以使用 .permissionAsker
属性
// make sure to not introduce retain cycles here
toggle.permissionAsker = { .defaultAlert(on: viewController) }
如果您想使用任何其他控件而不是系统 UISwitch
,您可以为 NotificationPermissionView
编写自己的适配器。 有关参考,请参阅 NotificationsPermissionView.swift
中的 __UISwitchAdapter
。
禁用组将删除所有待处理的通知,并阻止新的重新调度,直到再次授予权限
LocalNotifications.disable(group: DailyQuoteGroup())
let status = LocalNotifications.GroupLevelAuthorization.getCurrent(forGroup: DailyQuoteGroup().groupIdentifier)
switch status {
case .allowed: /* ... */
case .denied: /* ... */
case .notAsked: /* ... */
}
NotificationsTimeline
允许使用可用的 schedule(with:)
重载之一异步创建内容
final class DailyQuoteGroup: LocalNotificationsGroup {
let groupIdentifier: String = "dailyQuote"
func getTimeline(completion: @escaping (NotificationsTimeline) -> ()) {
let timeline = NotificationsTimeline {
EveryDay(forDays: 50, starting: .today)
.at(hour: 9, minute: 00)
.schedule(with: makeRandomQuoteContent(completion:))
}
completion(timeline)
}
private func makeRandomQuoteContent(completion: @escaping (NotificationContent) -> ()) {
QuoteStore.fetchRandom { (quote) in
let content = NotificationContent(
title: quote,
body: "Open app for more quotes",
sound: .default
)
completion(content)
}
}
}
其他可用的 .schedule
重载
.schedule(title: String? = nil, subtitle: String? = nil, body: String? = nil, sound: UNNotificationSound? = .default)
.schedule(with maker: @escaping () -> UNMutableNotificationContent?)
.schedule(with maker: @escaping (LocalNotifications.Trigger) -> UNMutableNotificationContent?)
.schedule(with maker: @escaping (_ nextTriggerDate: Date) -> UNMutableNotificationContent?)
.schedule(with asyncMaker: @escaping (_ trigger: LocalNotifications.Trigger, _ completion: @escaping (UNMutableNotificationContent?) -> Void) -> Void)
.schedule(with asyncMaker: @escaping (_ nextTriggerDate: Date, _ completion: @escaping (UNMutableNotificationContent?) -> Void) -> Void)
.schedule(with asyncMaker: @escaping (_ completion: @escaping (UNMutableNotificationContent?) -> Void) -> Void)
.schedule(with content: @autoclosure @escaping () -> UNMutableNotificationContent?)
默认情况下,getTimeline
将始终在主线程上调用。 如果您的应用程序逻辑允许在后台队列上调用 getTimeline
,请将 preferredExecutionContext
设置为 .canRunOnAnyQueue
final class DailyQuoteGroup: LocalNotificationsGroup {
let groupIdentifier: String = "dailyQuote"
var preferredExecutionContext: LocalNotificationsGroupContextPreference {
return .canRunOnAnyQueue
}
func getTimeline(completion: @escaping (NotificationsTimeline) -> ()) {
...
}
}
特别感谢