一、前言

远程推送通知是iOS开发中高频必备功能,绝大多数App都需要实现推送消息提醒、点击通知跳转指定业务页面。iOS推送分为三种运行状态,开发中必须全部兼容:

  1. 前台运行:App处于打开状态,直接接收推送弹窗

  2. 后台挂起:App未关闭、退至后台,点击通知唤起App并跳转

  3. 进程杀死:App彻底关闭、进程销毁,点击通知冷启动App并跳转

本文基于 Swift + iOS 10+,依托UserNotifications框架,提供一套企业级可直接复用的完整推送代码,包含权限注册、DeviceToken获取、三种状态推送监听、统一页面跳转逻辑,代码经过真机测试,开箱即用。

二、前期工程配置(必看)

2.1 开启推送能力

Xcode -> Targets -> Signing & Capabilities,添加以下能力:

  • Push Notifications:推送权限

  • Background Modes:后台模式,勾选 Remote notifications

2.2 依赖库说明

本文解析推送Json数据使用 SwiftyJSON,若无该依赖,可手动使用原生字典解析。

2.3 证书配置

远程推送必须配置APNs推送证书,开发环境使用开发证书,生产环境使用发布证书,否则无法获取DeviceToken、接收不到推送。

三、完整代码实现

3.1 引入依赖、遵守代理

import UIKit
import UserNotifications
import SwiftyJSON

@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // 1. 注册推送权限
        registerForPushNotifications(application: application)
        // 2. 处理App被杀死状态下,点击通知冷启动跳转
        handlePushWhenAppKilled(launchOptions: launchOptions)
        
        return true
    }
}

3.2 推送权限注册

兼容iOS10前后版本,申请弹窗、声音、角标权限,授权成功后注册远程推送,获取设备Token。

// MARK: - 注册推送权限
extension AppDelegate {
    
    func registerForPushNotifications(application: UIApplication) {
        if #available(iOS 10.0, *) {
            let center = UNUserNotificationCenter.current()
            center.delegate = self
            // 申请通知权限:提示框、声音、角标
            center.requestAuthorization(options: [.sound, .alert, .badge]) { granted, error in
                guard granted else {
                    print("用户拒绝通知权限")
                    return
                }
                // 权限授权成功,获取通知配置并注册Token
                self.getNotificationSettings()
            }
        } else {
            // iOS10 以下低版本适配
            let settings = UIUserNotificationSettings(types: [.sound, .alert, .badge], categories: nil)
            application.registerUserNotificationSettings(settings)
            application.registerForRemoteNotifications()
        }
    }
    
    // 获取通知授权状态,注册远程推送
    func getNotificationSettings() {
        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().getNotificationSettings { settings in
                guard settings.authorizationStatus == .authorized else { return }
                DispatchQueue.main.async {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            }
        }
    }
}

3.3 获取DeviceToken

DeviceToken是设备唯一推送标识,需要上传至服务端,用于精准推送。

// MARK: - DeviceToken 回调
extension AppDelegate {
    // 注册成功,获取设备Token
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
        print("推送DeviceToken:\(token)")
        // 此处上传Token至后端服务器
    }
    
    // 注册推送失败回调
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("推送注册失败:\(error.localizedDescription)")
    }
}

3.4 处理App杀死状态推送跳转

App进程彻底销毁时,点击通知会触发冷启动,通过launchOptions获取推送载荷,实现页面跳转。

// MARK: - App被杀死 点击通知冷启动跳转
extension AppDelegate {
    
    func handlePushWhenAppKilled(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
        guard let options = launchOptions else { return }
        // 获取远程推送数据
        if let notification = options[.remoteNotification] as? [String: Any] {
            print("APP杀死状态,点击通知启动:\(notification)")
            let userInfo = notification
            // 解析自定义推送字段
            let key = userInfo["key"] as? String ?? ""
            let value = userInfo["value"] as? Int ?? 0
            // 根据业务key判断跳转页面
            if key == "通知传过来的key值" {
                handlePushForNavigationTo(key: value)
            }
        }
    }
}

3.5 通用页面跳转工具方法

适配所有推送场景,自动获取当前顶层控制器,避免页面栈覆盖问题,采用Present方式跳转,同时标记通知来源。

// MARK: - 推送通用跳转方法
extension AppDelegate {
    
    func handlePushForNavigationTo(key: Int) {
        guard key > 0 else { return }
        // 获取根控制器
        guard let rootVC = window?.rootViewController else { return }
        var topVC = rootVC
        // 循环遍历,拿到当前最顶层控制器
        while let presentedVC = topVC.presentedViewController {
            topVC = presentedVC
        }
        // 实例化跳转目标控制器(根据自身项目修改)
        if let nextViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ViewController") as? ViewController {
            nextViewController.key值 = key
            // 标记来源:通知跳转
            nextViewController.isFromNotification = true
            let navController = UINavigationController(rootViewController: nextViewController)
            navController.modalPresentationStyle = .fullScreen
            // 顶层控制器弹出新页面
            topVC.present(navController, animated: true, completion: nil)
        }
    }
}

3.6 推送代理方法(前台/后台)

处理App前台接收推送、后台点击推送两种场景,统一解析推送参数、执行跳转逻辑。

// MARK: - UNUserNotificationCenter 推送代理
extension AppDelegate: UNUserNotificationCenterDelegate {
    
    // 后台、挂起状态:点击通知触发
    @available(iOS 10.0, *)
    func userNotificationCenter(_ center: UNUserNotificationCenter,
                                didReceive response: UNNotificationResponse,
                                withCompletionHandler completionHandler: @escaping () -> Void) {
        let userInfo = response.notification.request.content.userInfo
        let json = JSON(userInfo)
        let key = json["key"].stringValue
        let value = json["value"].intValue
        print("后台点击通知:key=\(key),value=\(value)")
        
        // 根据key跳转对应页面
        switch key {
        case "通知传过来的key值":
            handlePushForNavigationTo(key: value)
            break
        default:
            break
        }
        completionHandler()
    }
    
    // 前台运行状态:收到推送即时触发
    @available(iOS 10.0, *)
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo
        let json = JSON(userInfo)
        print("前台收到推送:\(userInfo)")
        print("推送key:\(json["key"].stringValue)")
        // 前台展示:弹窗、声音、角标
        completionHandler([.alert, .badge, .sound])
    }
}

四、推送三种状态逻辑总结

App运行状态 触发方法 使用场景
前台运行 willPresent App打开状态,直接弹窗展示推送
后台挂起 didReceive 退到后台,点击通知唤起App并跳转
进程杀死 launchOptions 彻底关闭App,冷启动解析推送跳转

五、开发注意事项(避坑指南)

5.1 权限相关

  • iOS13+ 需要在Info.plist添加权限描述:NSUserNotificationUsageDescription,否则安装直接崩溃;

  • 必须用户手动授权,静默授权无法实现。

5.2 DeviceToken问题

  • 模拟器无法获取DeviceToken,必须使用真机调试;

  • 重装App、更换设备、还原手机,DeviceToken会改变,后端需做兼容处理。

5.3 页面跳转坑点

  • 禁止直接获取根控制器跳转,必须循环遍历拿到当前最顶层控制器,防止页面被遮挡、跳转无反应;

  • 推送跳转建议使用Present模态弹出,不破坏原有项目导航栈。

5.4 推送载荷规范

  • 后端推送JSON必须包含aps标准字段,自定义业务字段放置同级;

  • 杀死状态下,iOS10及以上不再走代理方法,只能通过launchOptions解析数据。

5.5 后台推送限制

  • 未开启后台推送权限,App后台无法接收静默推送;

  • 系统会限制推送频率,频繁推送会被系统节流拦截。

六、总结

本文完整实现了iOS Swift远程推送全套功能,适配iOS10+所有机型,覆盖前台、后台、杀死三种核心场景。代码封装简洁、耦合度低,跳转逻辑通用,可直接接入企业项目。开发中重点注意:真机调试、证书配置、顶层控制器跳转、杀死状态数据解析四大关键点,规避推送常见Bug。

更多推荐