【SwiftUI提高练中学】第19课 推送与远程通知进阶:APNs、通信通知与专注过滤

本课目标

入门第17课你学过本地通知和后台任务。本课把通知做到生产级:远程推送、通知服务扩展、通知内容扩展、通信通知、专注过滤、分组与摘要、时间敏感与重要警报。完成后,你的"我的书库"能通过服务器推送新书提醒、显示带封面的通知、在专注模式下智能过滤,并支持用户不打开 App 就能操作。

一、核心概念

  • APNs:Apple Push Notification service,苹果推送服务,服务器通过它发推送。
  • Device Token:设备令牌,每次启动可能变化,必须上传服务器。
  • Payload :推送内容,JSON 格式,包含 aps 字典和自定义字段。
  • 通知服务扩展:在通知显示前修改内容,如解密、下载图片、添加附件。
  • 通知内容扩展:自定义通知展开后的界面。
  • 通信通知 :INSendMessageIntent,让通知显示为某人发来的消息,带头像。
  • 专注过滤 :SetFocusFilterIntent,让 App 在用户开启专注模式时调整行为。
  • 通知分组 :threadIdentifier 把相关通知归组。
  • 通知摘要:系统延迟非紧急通知,集中展示。
  • 时间敏感 :interruptionLevel = .timeSensitive,突破专注模式。
  • 重要警报 :criticalAlert,需要苹果特别授权,声音无法静音。
  • 临时授权 :provisional,先静默推送,用户可选择保留。

二、配置推送能力

1. 开启 Push Notifications

Xcode → App Target → Signing & Capabilities → + Capability → Push Notifications。

2. 开启 Background Modes

勾选 Remote notifications。

3. Info.plist

xml 复制代码
<key>UIBackgroundModes</key>
<array>
    <string>remote-notification</string>
</array>

4. 创建 APNs 密钥

在 Apple Developer 后台创建 APNs Auth Key,下载 .p8 文件,记录 Key ID 和 Team ID,交给服务器使用。

三、注册远程通知与获取 Token

swift 复制代码
import SwiftUI
import UserNotifications

// AppDelegate:处理远程通知注册
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        // 设置通知代理
        UNUserNotificationCenter.current().delegate = self

        // 注册通知分类
        registerNotificationCategories()

        return true
    }

    // 注册远程通知成功,拿到 device token
    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        // 转成十六进制字符串
        let token = deviceToken.map { String(format: "%02x", $0) }.joined()
        print("APNs token: \(token)")

        // 上传到你的服务器
        Task {
            await uploadToken(token)
        }
    }

    // 注册失败
    func application(
        _ application: UIApplication,
        didFailToRegisterForRemoteNotificationsWithError error: Error
    ) {
        print("注册远程通知失败:\(error)")
    }

    // 上传 token 到服务器
    func uploadToken(_ token: String) async {
        guard let url = URL(string: "https://api.example.com/device-token") else { return }
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try? JSONEncoder().encode(["token": token])

        _ = try? await URLSession.shared.data(for: request)
    }

    // 注册通知分类和操作
    func registerNotificationCategories() {
        // 新书通知分类
        let readAction = UNNotificationAction(
            identifier: "READ_NOW",
            title: "立即阅读",
            options: [.foreground]
        )
        let laterAction = UNNotificationAction(
            identifier: "LATER",
            title: "稍后提醒",
            options: []
        )

        let newBookCategory = UNNotificationCategory(
            identifier: "NEW_BOOK",
            actions: [readAction, laterAction],
            intentIdentifiers: [],
            options: [.customDismissAction]
        )

        UNUserNotificationCenter.current().setNotificationCategories([newBookCategory])
    }
}

// SwiftUI App 入口接入 AppDelegate
@main
struct MyLibraryApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    // 请求通知权限
                    let granted = try? await UNUserNotificationCenter.current()
                        .requestAuthorization(options: [.alert, .sound, .badge])
                    if granted == true {
                        // 在主线程注册远程通知
                        await MainActor.run {
                            UIApplication.shared.registerForRemoteNotifications()
                        }
                    }
                }
        }
    }
}

解读:registerForRemoteNotifications() 必须在主线程调用;token 每次启动都可能变化,必须重新上传;requestAuthorization 先请求用户授权。

四、APNs Payload 结构

标准 payload:

json 复制代码
{
  "aps": {
    "alert": {
      "title": "新书上架",
      "subtitle": "你关注的作者有新书",
      "body": "《SwiftUI 进阶》已加入书库"
    },
    "badge": 1,
    "sound": "default",
    "thread-id": "new-books",
    "category": "NEW_BOOK",
    "interruption-level": "time-sensitive",
    "mutable-content": 1,
    "content-available": 1
  },
  "bookID": "12345",
  "coverURL": "https://example.com/cover.jpg"
}

字段说明:

  • alert:标题、副标题、正文。
  • badge:角标数字。
  • sound:声音,"default" 或自定义。
  • thread-id:分组标识。
  • category:关联通知分类,显示操作按钮。
  • interruption-level:passive、active、time-sensitive、critical。
  • mutable-content:允许通知服务扩展修改内容。
  • content-available:静默推送,唤醒 App 后台处理。
  • 自定义字段:bookID、coverURL,供 App 和服务扩展使用。

五、处理通知:前台、点击、后台

swift 复制代码
extension AppDelegate {
    // App 在前台时收到通知
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification
    ) async -> UNNotificationPresentationOptions {
        // 前台也显示横幅、播放声音、更新角标
        return [.banner, .sound, .badge]
    }

    // 用户点击通知或操作按钮
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse
    ) async {
        let userInfo = response.notification.request.content.userInfo

        switch response.actionIdentifier {
        case "READ_NOW":
            // 打开 App 并跳转到书籍详情
            if let bookID = userInfo["bookID"] as? String {
                await MainActor.run {
                    NotificationCenter.default.post(
                        name: .openBook,
                        object: nil,
                        userInfo: ["bookID": bookID]
                    )
                }
            }
        case "LATER":
            // 重新调度本地通知
            scheduleLaterReminder()
        case UNNotificationDefaultActionIdentifier:
            // 用户点击通知本体
            print("用户点击了通知")
        case UNNotificationDismissActionIdentifier:
            // 用户滑掉了通知
            print("用户关闭了通知")
        default:
            break
        }
    }

    func scheduleLaterReminder() {
        let content = UNMutableNotificationContent()
        content.title = "阅读提醒"
        content.body = "你之前想稍后阅读的书还在等你"
        content.sound = .default

        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false)
        let request = UNNotificationRequest(
            identifier: UUID().uuidString,
            content: content,
            trigger: trigger
        )
        UNUserNotificationCenter.current().add(request)
    }
}

// 通知名称
extension Notification.Name {
    static let openBook = Notification.Name("openBook")
}

解读:willPresent 决定前台是否显示;didReceive 处理点击和操作按钮;自定义 userInfo 传递数据。

六、通知服务扩展:修改内容

File → New → Target → Notification Service Extension。

swift 复制代码
import UserNotifications
import UIKit

class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    // 收到通知,有机会修改内容
    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        self.contentHandler = contentHandler
        self.bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

        guard let content = bestAttemptContent else {
            contentHandler(request.content)
            return
        }

        // 下载封面图并添加为附件
        if let coverURLString = content.userInfo["coverURL"] as? String,
           let coverURL = URL(string: coverURLString) {
            downloadImage(from: coverURL) { imageURL in
                if let imageURL,
                   let attachment = try? UNNotificationAttachment(
                       identifier: "cover",
                       url: imageURL,
                       options: nil
                   ) {
                    content.attachments = [attachment]
                }
                contentHandler(content)
            }
        } else {
            contentHandler(content)
        }
    }

    // 超时未完成时调用
    override func serviceExtensionTimeWillExpire() {
        if let contentHandler, let content = bestAttemptContent {
            contentHandler(content)
        }
    }

    // 下载图片
    func downloadImage(from url: URL, completion: @escaping (URL?) -> Void) {
        URLSession.shared.downloadTask(with: url) { localURL, _, _ in
            guard let localURL else {
                completion(nil)
                return
            }

            // 移动到临时目录并加扩展名
            let tempURL = FileManager.default.temporaryDirectory
                .appendingPathComponent(UUID().uuidString)
                .appendingPathExtension("jpg")

            try? FileManager.default.moveItem(at: localURL, to: tempURL)
            completion(tempURL)
        }.resume()
    }
}

解读:服务扩展在通知显示前运行,可以解密、下载图片、修改文案。mutable-content: 1 才会触发。

七、通知内容扩展:自定义界面

File → New → Target → Notification Content Extension。

swift 复制代码
import UserNotifications
import UIKit
import SwiftUI

class NotificationViewController: UIViewController, UNNotificationContentExtension {
    var hostingController: UIHostingController<BookNotificationView>?

    func didReceive(_ notification: UNNotification) {
        let userInfo = notification.request.content.userInfo
        let title = notification.request.content.title
        let body = notification.request.content.body
        let bookID = userInfo["bookID"] as? String ?? ""

        // 用 SwiftUI 渲染自定义通知界面
        let view = BookNotificationView(
            title: title,
            body: body,
            bookID: bookID
        )

        let hosting = UIHostingController(rootView: view)
        hostingController = hosting

        addChild(hosting)
        view.addSubview(hosting.view)
        hosting.didMove(toParent: self)

        hosting.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hosting.view.topAnchor.constraint(equalTo: view.topAnchor),
            hosting.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hosting.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hosting.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
}

struct BookNotificationView: View {
    let title: String
    let body: String
    let bookID: String

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(title).font(.headline)
            Text(body).font(.subheadline)
            HStack {
                Button("查看") {
                    // 打开 App
                }
                Button("收藏") {
                    // 收藏逻辑
                }
            }
            .buttonStyle(.bordered)
        }
        .padding()
    }
}

解读:内容扩展在用户长按或下拉通知时显示自定义界面,适合展示封面、按钮和更多信息。

八、通信通知:显示为消息

通信通知让通知显示为"某人发来消息",带头像,适合社交、协作类 App。

swift 复制代码
import Intents
import UserNotifications

// 发送通信通知
func sendCommunicationNotification(from sender: String, body: String) async throws {
    // 1. 创建发送者头像
    let avatar = INImage(named: "avatar") // 或从 URL 创建

    // 2. 创建发送者
    let handle = INPersonHandle(value: sender, type: .unknown)
    let person = INPerson(
        personHandle: handle,
        nameComponents: nil,
        displayName: sender,
        image: avatar,
        contactIdentifier: nil,
        customIdentifier: sender
    )

    // 3. 创建消息意图
    let intent = INSendMessageIntent(
        recipients: nil,
        outgoingMessageType: .outgoingMessageText,
        content: body,
        speakableGroupName: nil,
        conversationIdentifier: "chat_\(sender)",
        serviceName: nil,
        sender: person,
        attachments: nil
    )

    // 4. 生成通知内容
    let interaction = INInteraction(intent: intent, response: nil)
    let content = try await UNMutableNotificationContent.updating(from: intent)

    // 5. 发送通知
    let request = UNNotificationRequest(
        identifier: UUID().uuidString,
        content: content,
        trigger: nil
    )
    try await UNUserNotificationCenter.current().add(request)
}

解读:INSendMessageIntent 让通知显示为消息,系统自动处理头像和名称。需要在 Info.plist 中声明 NSUserActivityTypes 包含 INSendMessageIntent。

九、专注过滤:Focus Filter

专注过滤让 App 在用户开启"工作""睡眠"等专注模式时调整行为。

swift 复制代码
import AppIntents
import SwiftUI

// 专注过滤器
struct LibraryFocusFilter: SetFocusFilterIntent {
    static var title: LocalizedStringResource = "书库专注过滤"
    static var description = IntentDescription("在专注模式下只显示收藏的书籍")

    // 可配置参数
    @Parameter(title: "只显示收藏", default: true)
    var onlyFavorites: Bool

    @Parameter(title: "隐藏已读", default: false)
    var hideRead: Bool

    // 应用过滤
    func perform() async throws -> some IntentResult {
        // 保存过滤设置
        UserDefaults.standard.set(onlyFavorites, forKey: "focus_onlyFavorites")
        UserDefaults.standard.set(hideRead, forKey: "focus_hideRead")

        // 通知界面刷新
        await MainActor.run {
            NotificationCenter.default.post(name: .focusFilterChanged, object: nil)
        }

        return .result()
    }
}

extension Notification.Name {
    static let focusFilterChanged = Notification.Name("focusFilterChanged")
}

在视图中响应:

swift 复制代码
struct LibraryView: View {
    @AppStorage("focus_onlyFavorites") private var onlyFavorites = false
    @AppStorage("focus_hideRead") private var hideRead = false
    @Query private var books: [Book]

    var filteredBooks: [Book] {
        var result = books
        if onlyFavorites {
            result = result.filter { $0.isFavorite }
        }
        if hideRead {
            result = result.filter { !$0.isRead }
        }
        return result
    }

    var body: some View {
        List(filteredBooks) { book in
            BookRow(book: book)
        }
        .onReceive(NotificationCenter.default.publisher(for: .focusFilterChanged)) { _ in
            // 刷新界面
        }
    }
}

解读:SetFocusFilterIntent 让 App 出现在专注模式设置中,用户可以为不同专注模式配置不同行为。

十、分组、摘要、时间敏感与重要警报

1. 分组

swift 复制代码
content.threadIdentifier = "new-books" // 相同线程归为一组
content.summaryArgument = "新书" // 摘要中显示
content.summaryArgumentCount = 3 // 数量

2. 时间敏感

swift 复制代码
if #available(iOS 15.0, *) {
    content.interruptionLevel = .timeSensitive // 突破专注模式
}

3. 重要警报

swift 复制代码
// 需要苹果特别授权
content.interruptionLevel = .critical
content.sound = UNNotificationSound.defaultCritical

4. 临时授权

swift 复制代码
let granted = try await UNUserNotificationCenter.current()
    .requestAuthorization(options: [.provisional, .alert, .sound])
// 静默推送,用户在通知中心可以选择"保持"

十一、测试与调试

1. 模拟器推送

创建 push.apns 文件:

json 复制代码
{
  "Simulator Target Bundle": "com.yourname.mylibrary",
  "aps": {
    "alert": {
      "title": "测试推送",
      "body": "这是一条测试通知"
    },
    "sound": "default",
    "badge": 1
  }
}

拖到模拟器即可触发。

2. 命令行推送

bash 复制代码
xcrun simctl push booted com.yourname.mylibrary push.apns

3. 真机测试

真机推送需要 APNs 证书或密钥,服务器通过 HTTP/2 发送。可以用 Xcode 的 Push Notifications 控制台测试,或使用第三方工具如 Pusher。

4. 常见错误

  • 未注册远程通知 :检查 registerForRemoteNotifications 是否调用。
  • token 未上传:服务器没有 token 发不了。
  • payload 格式错误 :aps 必须存在,字段类型正确。
  • 服务扩展未触发 :检查 mutable-content: 1。
  • 权限被拒:引导用户去设置开启。

十二、习题、答案与解读

选择题

题1

题目:注册远程通知使用哪个方法?

A. registerForRemoteNotifications() B. requestAuthorization() C. add(request) D. setNotificationCategories()

要求:选出正确选项。

答案:A

解读:在主线程调用 UIApplication.shared.registerForRemoteNotifications()。

题2

题目:获取 device token 的代理方法是?

A. didRegisterForRemoteNotificationsWithDeviceToken B. didReceive C. willPresent D. didFinishLaunching

要求:选出正确选项。

答案:A

解读:在 AppDelegate 中实现,拿到 token 后上传服务器。

题3

题目:让通知服务扩展修改内容,payload 需要哪个字段?

A. mutable-content: 1 B. content-available: 1 C. category D. thread-id

要求:选出正确选项。

答案:A

解读:mutable-content 为 1 时触发服务扩展。

题4

题目:通信通知使用哪个 intent?

A. INSendMessageIntent B. INStartCallIntent C. INSearchForMessagesIntent D. INSetMessageAttributeIntent

要求:选出正确选项。

答案:A

解读:INSendMessageIntent 让通知显示为消息。

题5

题目:专注过滤使用哪个协议?

A. SetFocusFilterIntent B. AppIntent C. WidgetConfigurationIntent D. TimelineProvider

要求:选出正确选项。

答案:A

解读:SetFocusFilterIntent 让 App 接入专注模式。

判断题

题1

题目:device token 获取一次后可以永久使用。

要求:判断对错。

答案:错

解读:token 每次启动可能变化,必须重新上传。

题2

题目:willPresent 决定 App 在前台时是否显示通知。

要求:判断对错。

答案:对

解读:返回 [.banner, .sound, .badge] 显示,返回 [] 静默。

题3

题目:通知服务扩展可以下载图片并添加为附件。

要求:判断对错。

答案:对

解读:服务扩展在通知显示前运行,适合富媒体处理。

题4

题目:重要警报不需要苹果特别授权。

要求:判断对错。

答案:错

解读:criticalAlert 需要苹果特别批准。

题5

题目:content-available: 1 是静默推送,唤醒 App 后台处理。

要求:判断对错。

答案:对

解读:静默推送不显示通知,用于后台同步。

填空题

题1

题目:注册远程通知使用 ______()。

要求:填入空白处。

答案:registerForRemoteNotifications

解读:在主线程调用。

题2

题目:获取 token 的代理方法是 didRegisterForRemoteNotificationsWith______。

要求:填入空白处。

答案:DeviceToken

解读:参数是 Data。

题3

题目:让服务扩展修改内容设置 mutable-content: ______。

要求:填入空白处。

答案:1

解读:为 1 时触发。

题4

题目:通信通知使用 ______ intent。

要求:填入空白处。

答案:INSendMessageIntent

解读:Intents 框架。

题5

题目:专注过滤实现 ______ 协议。

要求:填入空白处。

答案:SetFocusFilterIntent

解读:AppIntents 框架。

改错题

题1

题目:

swift 复制代码
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.description
    print(token)
}

要求:找出 token 转换问题并改正。

答案:

swift 复制代码
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02x", $0) }.joined()
    print(token)
}

解读:Data.description 不是标准 token 格式,需要转十六进制字符串。

题2

题目:

swift 复制代码
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in
    UIApplication.shared.registerForRemoteNotifications()
}

要求:找出线程问题并改正。

答案:

swift 复制代码
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in
    DispatchQueue.main.async {
        UIApplication.shared.registerForRemoteNotifications()
    }
}

解读:registerForRemoteNotifications 必须在主线程调用。

题3

题目:

swift 复制代码
let content = UNMutableNotificationContent()
content.title = "新书"
content.body = "上架了"
// 期望显示操作按钮,但没有

要求:找出缺少的配置。

答案:

swift 复制代码
content.categoryIdentifier = "NEW_BOOK" // 关联分类
// 并确保已注册 UNNotificationCategory

解读:通知需要设置 categoryIdentifier 并注册对应分类,才会显示操作按钮。

代码预测题

题目:

swift 复制代码
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification
) async -> UNNotificationPresentationOptions {
    return []
}

要求:App 在前台收到通知时会怎样?

答案:不显示任何横幅、声音或角标。

解读:返回空数组表示前台静默。

编程题

题1

题目:注册远程通知并上传 token。

要求:请求权限后注册,拿到 token 后打印并上传。

参考代码:

swift 复制代码
func requestAndRegister() async {
    let center = UNUserNotificationCenter.current()
    let granted = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
    guard granted == true else { return }

    await MainActor.run {
        UIApplication.shared.registerForRemoteNotifications()
    }
}

func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
    let token = deviceToken.map { String(format: "%02x", $0) }.joined()
    Task { await uploadToken(token) }
}

解读:权限 → 注册 → 拿 token → 上传,完整流程。

题2

题目:写通知服务扩展,下载图片并添加附件。

要求:从 coverURL 下载,添加为通知附件。

参考代码:

swift 复制代码
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    self.contentHandler = contentHandler
    bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent

    guard let content = bestAttemptContent,
          let urlString = content.userInfo["coverURL"] as? String,
          let url = URL(string: urlString) else {
        contentHandler(request.content)
        return
    }

    URLSession.shared.downloadTask(with: url) { localURL, _, _ in
        if let localURL {
            let temp = FileManager.default.temporaryDirectory
                .appendingPathComponent(UUID().uuidString)
                .appendingPathExtension("jpg")
            try? FileManager.default.moveItem(at: localURL, to: temp)
            if let attachment = try? UNNotificationAttachment(identifier: "cover", url: temp) {
                content.attachments = [attachment]
            }
        }
        contentHandler(content)
    }.resume()
}

解读:服务扩展下载图片,添加附件,通知会显示图片。

题3

题目:挑战------实现专注过滤器。

要求:用户在专注模式中配置"只显示收藏",App 保存并刷新。

参考代码:

swift 复制代码
struct LibraryFocusFilter: SetFocusFilterIntent {
    static var title: LocalizedStringResource = "书库专注过滤"

    @Parameter(title: "只显示收藏", default: true)
    var onlyFavorites: Bool

    func perform() async throws -> some IntentResult {
        UserDefaults.standard.set(onlyFavorites, forKey: "focus_onlyFavorites")
        await MainActor.run {
            NotificationCenter.default.post(name: .focusFilterChanged, object: nil)
        }
        return .result()
    }
}

在视图中读取 @AppStorage("focus_onlyFavorites") 并过滤。

解读:专注过滤器让 App 随用户状态变化。

开放思考题

题1

题目:本地通知和远程推送有什么区别?

要求:简要回答。

参考:本地通知由 App 在设备上调度;远程推送由服务器通过 APNs 发送。

解读:提醒用本地,消息和营销用远程。

题2

题目:为什么 device token 必须上传服务器?

要求:简要回答。

参考:服务器需要 token 才能通过 APNs 定位到具体设备发送推送。

解读:token 是设备的推送地址。

题3

题目:通知服务扩展和内容扩展有什么区别?

要求:简要回答。

参考:服务扩展在显示前修改内容;内容扩展自定义展开后的界面。

解读:服务扩展处理数据,内容扩展处理展示。

十三、全课知识点总结

  • APNs:苹果推送服务,服务器通过 HTTP/2 发送。
  • Device Token:设备令牌,每次启动可能变化,必须上传。
  • 注册流程:请求权限 → registerForRemoteNotifications → 获取 token → 上传。
  • Payload:aps 字典 + 自定义字段。
  • mutable-content: 1:触发通知服务扩展。
  • content-available: 1:静默推送,后台处理。
  • thread-id:通知分组。
  • category:关联通知分类,显示操作按钮。
  • interruption-level:passive、active、time-sensitive、critical。
  • 通知服务扩展:修改内容、下载图片、添加附件。
  • 通知内容扩展:自定义通知展开界面。
  • 通信通知:INSendMessageIntent,显示头像和消息样式。
  • 专注过滤:SetFocusFilterIntent,随专注模式调整行为。
  • 时间敏感突破专注模式,重要警报需苹果授权。
  • 临时授权:.provisional 静默推送。
  • 测试:simctl push、Xcode 控制台、真机 + 服务器。
  • 常见错误:token 未上传、payload 格式、权限被拒、扩展未触发。

本课达标标准 :

能配置推送能力并注册远程通知,能获取并上传 device token,能构造和理解 APNs payload,能处理前台展示和用户点击,能写通知服务扩展修改内容,能写通知内容扩展自定义界面,能发送通信通知,能实现专注过滤器,能使用分组、时间敏感和重要警报,并能通过 simctl 和真机测试推送。

相关推荐
传奇开心果编程9 小时前
【SwiftUI提高练中学】第9课 安全与隐私
swiftui·移动开发·swift·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程19 小时前
【声明式UI开发实用技术学与练】第10课 用状态机建模复杂交互
移动开发·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程20 小时前
【声明式UI实用开发技术学与练】第5课 表单处理:受控与非受控
ui·移动开发·开发范式·编程框架·声明式ui
传奇开心果编程21 小时前
【声明式UI开发实用技术学与练】第9课 派生状态与计算
ui·移动开发·ux·编程框架·声明式ui
爱编程的小新☆21 天前
Fake GPS 虚拟定位保姆级使用教学
android·移动开发·gps·虚拟定位
Kevin Coding1 个月前
鸿蒙 emitter/EventHub 没有 Sticky 粘性事件?手写一个轻量级 EmitterManager 解决
前端·华为·前端框架·移动开发·harmonyos
天府云创1 个月前
uni-app X 蒸汽模式发布!性能比肩原生~
前端框架·uni-app·移动开发·原生应用·蒸汽模式·app研发·多端适配
m0_749690232 个月前
【寻迹校园 HarmonyOS NEXT 实战 01】从校园痛点到可上架 MVP:失物招领应用产品设计
人工智能·深度学习·移动开发·harmonyos·arkts·arkui·产品设计
m0_749690232 个月前
【寻迹校园 HarmonyOS NEXT 实战 03】NavPathStack 路由实战:18 个页面如何集中管理
华为·移动开发·harmonyos·arkui·navigation·navpathstack