接入离线推送功能

最近更新时间:2026-02-28 10:25:44

我的收藏
本文档将指导您如何基于 TIMPush 服务实现 VoIP 推送功能,适用于使用 AtomicXCore 进行无 UI 集成的场景。
注意:
使用 VoIP 推送功能首先需要 开通 TIMPush 服务。
LiveCommunicationKit 需要在 iOS 17.4 及以上系统中使用。
VoIP Push 无法复用 APNs 普通推送证书,需要单独在苹果开发者网站上 申请 VoIP Push 证书

集成效果

按照本文档接入 Apple LiveCommunicationKit 后,您的 App 将瞬间拥有微信同款的原生通话能力,在进程关闭或锁屏状态下,也能无差别唤醒系统级全屏接听界面:
接听前效果
接听后效果





准备条件

开通服务

进入 IM 控制台 > 插件市场,单击立即购买免费试用(每个应用可免费试用一次,有效期7天)。

注意:
推送插件试用或购买到期后,将自动停止提供推送服务(包括普通消息离线推送、全员/标签推送等服务)。为避免影响您业务的正常使用,请提前 购买/续费

厂商配置

步骤1:申请 VoIP Push 证书

在申请 VoIP Push 证书之前,请先登录 苹果开发者中心 网站开启 App 的远程推送功能。当您的 AppID 具备了 Push Notification 能力后,按照如下步骤申请并配置 VoIP Push 证书:
1. 登录 苹果开发者中心 网站,单击 Certificates, IDs & Profiles 选项卡,进入 Certificates, Identifiers & Profiles 页面。

2. 单击 Certificates 右侧的+

3. Create a New Certificate 选项卡中,选择 VoIP Services Certificate,并单击 Continue

4. Select an App ID for your VoIP Service Certificate 选项卡中,选择您当前的 App 的 BundleID,并单击 Continue
5. 紧接着,系统提示我们需要一个 Certificate Signing Request(CSR)
6. 我们接下来制作 CSR 文件。首先在 Mac 上打开钥匙串访问工具(Keychain Access),在菜单中选择钥匙串访问 > 证书助理 > 从证书颁发机构请求证书Keychain Access - Certificate Assistant - Request a Certificate From a Certificate Authority)。

7. 输入用户电子邮件地址(您的邮箱)、常用名称(您的名称或公司名),选择存储到磁盘,单击继续,系统将生成一个 *.certSigningRequest 文件。
返回上述步骤 5 中 Apple Developer 网站刚才的页面,单击 Choose File 上传生成的*.certSigningRequest文件。

8. 单击 Continue 后生成证书,点击 Download 下载对应的证书到本地。
9. 双击打开刚才下载的 voip_services.cer,系统会将其导入钥匙串中。
10. 打开钥匙串应用,在登录 > 我的证书,右键导出刚创建的 VoIP Services 的 P12 文件。
说明:
保存P12文件时,请务必为其设置密码。

步骤2:上传证书到 IM 控制台

打开 IM 控制台,选择您创建的 IM 应用,并按照如下步骤上传证书:
1. 选择您的 IM 应用,进入应用的接入设置。

2. 厂商配置中,切换到 iOS, 点击添加证书按钮,然后在悬浮页面中选择证书类型,上传 iOS 证书(p12),设置证书密码,单击确认

说明:
添加证书时,推送类型默认为 APNs,不影响 VoIP 证书的上传和使用。
VoIP Push 证书本身不区分生产环境和测试环境,生产环境和开发环境使用的是同一份 VoIP Push 证书,请分别上传。
上传证书名最好使用全英文(尤其不能使用括号等特殊字符)。
上传证书需要设置密码,无密码收不到推送。
发布 App Store 的证书需要设置为生产环境,否则无法收到推送。
上传的 p12 证书必须是自己申请的真实有效的证书。
3. 上传完成后,记录不同环境下的证书 ID。

说明:
开发环境和生产环境下的证书 ID 要严格区分,请根据实际环境填写。

功能接入

步骤1:完成工程配置

1. 如下图,请在您工程的 Capability 中添加 Push Notification 能力。



2. 如下图,请您在工程 Capability 的 Background Modes 中,开启 Voice over IP 选项。




步骤2: 添加依赖

1. 添加 Pod 依赖:在您项目的 Podfile 文件中添加 pod 'TUICore' 依赖。
pod 'TUICore'
2. 安装组件:在终端进入到 Podfile 文件所在的目录,然后执行以下命令安装组件。
pod install --repo-update

步骤3:初始化 PushKit 并设置证书 ID

1. 创建一个类来管理 VoIP 推送,初始化 PushKit 用于接收 VoIP 推送通知。
import PushKit
import ImSDK_Plus
import LiveCommunicationKit

class VoIPPushManager: NSObject {
private let pushRegistry: PKPushRegistry
private var certificateID: Int = 0
private var voipToken: Data? // 保存 Token,等待 TUILogin 成功后上报
// 存储当前通话的 UUID
private var currentCallUUID: UUID?
// ConversationManager 用于展示系统来电界面(步骤 7)
private lazy var conversationManager: ConversationManager = {
let configuration = ConversationManager.Configuration(
ringtoneName: "phone_ringing.mp3", // 来电铃声文件名
iconTemplateImageData: nil, // 来电图标(可选)
supportsVideo: true // 是否支持视频通话
)
let manager = ConversationManager(configuration: configuration)
manager.delegate = self
return manager
}()
override init() {
pushRegistry = PKPushRegistry(queue: DispatchQueue.main)
super.init()
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP]
// 监听 TUILogin 成功的通知
NotificationCenter.default.addObserver(
self,
selector: #selector(onTUILoginSuccess),
name: NSNotification.Name("TUILoginSuccess"),
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// 设置证书 ID
func setCertificateID(_ certificateID: Int) {
self.certificateID = certificateID
}
// TUILogin 成功后上报 Token
@objc private func onTUILoginSuccess() {
// 收到 TUILogin 成功通知,开始上报 VoIP Token
uploadVoIPToken()
}
// 上报 VoIP Token 到 IM 后台
private func uploadVoIPToken() {
guard let token = voipToken else {
// VoIP Token 尚未获取
return
}
guard certificateID != 0 else {
// 证书 ID 未设置
return
}
let config = V2TIMVOIPConfig()
config.certificateID = certificateID
config.token = token
V2TIMManager.sharedInstance().setVOIP(config: config, succ: {
// VoIP Token 上报成功
}, fail: { code, desc in
// VoIP Token 上报失败
})
}
}

2. AppDelegate 中初始化并设置证书 ID
class AppDelegate: UIResponder, UIApplicationDelegate {
private lazy var voipPushManager: VoIPPushManager = {
let manager = VoIPPushManager()
manager.setCertificateID(1234) // 替换为您在 IM 控制台获取的证书 ID
return manager
}()
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 触发 voipPushManager 的初始化
_ = voipPushManager
return true
}
}

注意:
certificateID 是您在 IM 控制台上传证书 后获取的证书 ID。
开发环境和生产环境的证书 ID 要严格区分,请根据实际环境填写。

步骤4:获取并保存 VoIP Token

实现 PKPushRegistryDelegate 代理方法,在获取到 VoIP Token 后先保存,等待 TUILogin 成功后再上报。
import PushKit
import ImSDK_Plus

extension VoIPPushManager: PKPushRegistryDelegate {
// 获取到 VoIP Token 后保存(不立即上报)
func pushRegistry(_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType) {
guard type == .voIP else { return }
// 保存 token
voipToken = pushCredentials.token
// 将 token 转换为字符串(用于日志)
let tokenString = pushCredentials.token.map { String(format: "%02.2hhx", $0) }.joined()
// VoIP Token 已获取:
// 如果 TUILogin 已经完成,立即上报;否则等待 TUILoginSuccess 通知
uploadVoIPToken()
}
// Token 失效
func pushRegistry(_ registry: PKPushRegistry,
didInvalidatePushTokenFor type: PKPushType) {
// VoIP Token 已失效
}
}
注意:
VoIP Token 的上报必须在 IM SDK 登录成功后才能进行,因此不能在获取 Token 时立即上报。

步骤5:Token 上报

在用户登录时,需要先登录 IM SDK,再登录 TUICore。TUICore 登录成功后会发送 TUILoginSuccess 通知,VoIPPushManager 监听到该通知后会自动上报 VoIP Token。
import TUICore
import AtomicXCore

// 步骤1:登录 IM SDK(AtomicXCore 的 LoginStore)
LoginStore.shared.login(
sdkAppID: sdkAppID,
userID: userID,
userSig: userSig
) { result in
switch result {
case .success:
// IM SDK 登录成功
// 步骤2:登录 TUICore
TUILogin.login(
sdkAppID,
userID: userID,
userSig: userSig
) {
// TUICore 登录成功
// 发送通知,触发 VoIP Token 上报
NotificationCenter.default.post(
name: NSNotification.Name("TUILoginSuccess"),
object: nil
)
} fail: { code, message in
// TUICore 登录失败
}
case .failure(let error):
// IM SDK 登录失败
}
}

步骤6:接收 VoIP 推送通知

extension VoIPPushManager: PKPushRegistryDelegate {
// 收到 VoIP 推送
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType) {
guard type == .voIP else { return }
// 必须在 5 秒内向系统报告来电,否则会 crash
showIncomingCall(with: payload) // 步骤 7
}
}

步骤7:展示来电通知

使用 LiveCommunicationKit 向系统报告来电,系统自动展示原生来电界面
import LiveCommunicationKit

extension VoIPPushManager {
// 向系统报告来电
private func showIncomingCall(with payload: PKPushPayload) {
// 解析推送数据
let payloadDict = payload.dictionaryPayload
// 1. 获取来电者名称(必需字段)
guard let callerName = payloadDict["voip_caller_name"] as? String else {
// 缺少 voip_caller_name 字段
return
}
// 2. 解析 ext 字段获取通话类型(音频/视频)
var isVideoCall = false
if let extString = payloadDict["ext"] as? String,
let extData = extString.data(using: .utf8),
let extDic = try? JSONSerialization.jsonObject(with: extData) as? [String: Any],
let voipExtDic = extDic["voip_ext"] as? [String: String],
let mediaType = voipExtDic["voip_media_type"] {
isVideoCall = (mediaType == "video")
}
// 创建来电通知内容
let uuid = UUID()
currentCallUUID = uuid // 保存 UUID,用于后续操作
var update = Conversation.Update(
members: [Handle(type: .generic, value: callerName, displayName: callerName)]
)
update.capabilities = isVideoCall ? .video : .playingTones
// 向系统报告来电(系统自动展示原生来电界面)
Task {
do {
try await conversationManager.reportNewIncomingConversation(uuid: uuid, update: update)
// 报告成功
} catch {
// 报告失败
}
}
}
}

// MARK: - ConversationManagerDelegate
extension VoIPPushManager: ConversationManagerDelegate {
func conversationManager(_ manager: ConversationManager, perform action: ConversationAction) {
if action is JoinConversationAction {
// 用户点击"接听",调用 CallStore 接听通话
CallStore.shared.accept { result in
switch result {
case .success:
// 接听成功,可以在这里跳转到通话页面
case .failure(let error):
}
}
} else if action is EndConversationAction {
// 用户点击"挂断",调用 CallStore 挂断通话
CallStore.shared.hangup { result in
switch result {
case .success:
case .failure(let error):
}
}
}
// 必须调用 fulfill() 通知系统操作已完成
action.fulfill()
}
}
您可以通过配置 Conversation.Update 的以下参数,自定义来电界面显示的来电者信息、通话类型等内容:
参数
类型
是否必需
说明
members
[Handle]
必需
来电者信息数组,支持多人通话场景。
每个 Handle 包含:
type:通常使用 .generic
value:来电者唯一标识(建议使用用户 ID)
displayName:显示在来电界面的名称
capabilities
Conversation.Capabilities
必需
通话类型:
.video:视频通话(显示视频图标)
.playingTones:语音通话(显示语音图标)
说明:
用户点击"接听"或"挂断"后,系统会回调 ConversationManagerDelegate 的方法,您需要在回调中处理接听/挂断的业务逻辑(如跳转到通话页面、调用 CallStore API 等)。

发起 VoIP 通话

发起通话

AtomicXCoreCallStore 已经默认使用 VoIP 推送,您不需要手动设置 offlinePushInfo 参数。CallStore 内部会自动配置以下推送信息:
推送类型:.voIP
推送标题:当前用户 ID
推送描述:"You have a new call"
iOS 铃声:phone_ringing.mp3
Swift
import AtomicXCore

// 构造通话参数(不需要设置 offlinePushInfo)
var params = CallParams()
params.timeout = 30 // 超时时间(秒)
params.userData = "自定义数据" // 扩展信息(可选)

// 发起通话
let userIdList = ["被叫用户的 userID"]
let mediaType = CallMediaType.video // .video 视频通话,.audio 语音通话

CallStore.shared.calls(
participantIds: userIdList,
callMediaType: mediaType,
params: params
) { result in
switch result {
case .success:
// 通话发起成功
// 此时可以跳转到您自己的通话页面
case .failure(let error):
// 通话失败处理
}
}
说明:
VoIP 推送降级策略
当使用 VoIP 推送时,后台会智能判断证书类型,保证用户仍然能收到推送通知:
上传的是 VoIP 证书:发送 VoIP Push。
上传的是 APNs 证书:自动降级为 APNs 推送。

接听通话

当被叫用户离线时收到 VoIP 推送并点击接听,系统会唤醒您的 App。此时需要监听通话事件并跳转到您自己实现的通话页面。
import AtomicXCore
import Combine

class YourViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 监听来电事件
CallStore.shared.callEventPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
guard let self = self else { return }
switch event {
case .onCallReceived(let callId, let mediaType, let userData):
// 收到来电,跳转到您自己的通话页面
self.showCallViewController(callId: callId, mediaType: mediaType, userData: userData)
case .onCallEnded(let callId, let mediaType, let reason, let userId):
// 通话结束,返回或更新 UI
self.dismissCallViewController()
case .onCallStarted(let callId, let mediaType):
// 通话已开始
break
}
}
.store(in: &cancellables)
}
private func showCallViewController(callId: String, mediaType: CallMediaType, userData: String) {
// 跳转到您自己实现的通话页面
let callVC = YourCallViewController()
callVC.callId = callId
callVC.mediaType = mediaType
callVC.userData = userData
callVC.modalPresentationStyle = .fullScreen
present(callVC, animated: true)
}
private func dismissCallViewController() {
// 关闭通话页面
dismiss(animated: true)
}
}

常见问题

获取不到 VoIP Push,如何处理?

1. 检查登录顺序:必须先登录 IM SDK,再登录 TUICore,否则 VoIP Token 无法上报。参考 步骤 5
2. 首先检查下 App 的运行环境和证书的环境是否一致,证书 ID 是否匹配,如果不一致,无法收到推送。
3. 请确认当前您登录的账号是否处于离线状态:按 home 键切后台、登录后主动杀进程退出。VoIP Push 目前只支持离线状态下的推送。
4. 检查 完成工程配置 是否正确。
5. 尝试重启测试手机来清除系统缓存和内存(很重要)。
6. IM 控制台提供 推送排查 工具,帮助您快速排查推送问题,使用方式详见:Push 排查工具。(很重要)

应用在收到 VoIP 推送后 crash 怎么办?

这通常是因为没有在 5 秒内向系统发送展示通知的请求。请确保在 didReceiveIncomingPushWith 回调中立即调用 reportNewIncomingConversation 方法。

如何自定义来电通知的内容?

您可以通过 Conversation.Update 来自定义来电通知的内容,包括:
来电者信息(displayName
通话类型(capabilities:.video.playingTones
其他自定义信息
具体用法请参考 Apple LiveCommunicationKit 官方文档。

交流与反馈

如果您在使用过程中,有什么建议或者意见,可以 联系我们,感谢您的反馈。