打造实时沟通体验
Create live communication experiences
2026年6月10日
一句话判断
✅ CallKit 终于迎来了现代重构,LiveCommunicationKit 用统一的 Action 机制彻底解决了 VoIP 应用系统 UI 与 App 内 UI 状态撕裂的顽疾。
这场 Session 讲了什么
做过音视频通话的开发者都知道,旧的 CXProvider (CallKit 的核心类) 就像个黑盒,系统 UI 和 App 内部状态经常对不上,而且对群组通话和灵动岛的支持一直像在打补丁。Apple 这次直接掀桌子,推出了 LiveCommunicationKit。
新框架把核心概念从“电话 (Call)”变成了“对话 (Conversation)”。你只需要维护一个 ConversationManager 单例,通过 ConversationAction 来统一接管所有交互。不管是用户在锁屏全屏 UI 点了接听,还是在 App 里点了挂断,甚至是在灵动岛上滑动结束,都会变成同一个 Action 丢给你的 Delegate。这对所有做 1v1 语音、多人语音房、甚至游戏内实时沟通的 App 来说,是一次底层架构级的减负。
值得深挖的点
Action 驱动的统一状态机
旧版 CallKit 最让人头疼的就是状态同步。用户在系统来电界面点了“拒绝”,App 内部可能还没反应过来,导致 UI 卡死或者信令错乱。LiveCommunicationKit 引入了 ConversationAction (对话动作) 机制。
Apple 的设计很聪明:他们把系统 UI 和 App 内 UI 的操作抽象成了同一种对象。当系统 UI 触发操作时,系统生成 Action 发给你的 ConversationManagerDelegate;当你在 App 内触发操作时,你自己构造 Action 传给 Manager。所有的业务逻辑都收敛在 perform action: 这一个回调里。这种设计强制开发者使用单一数据源(Single Source of Truth),彻底消灭了“系统以为挂了,App 以为还在通”的幽灵 Bug。代价是你的 Delegate 里会充满 switch 语句,但比起排查状态机死锁,这完全值得。
从 Call 到 Conversation 的范式转移
API 命名从 Call 变成 Conversation 不是文字游戏。Handle (标识符) 和 Capabilities (能力) 的解耦是这次重构的精髓。
在旧方案里,视频通话和语音通话往往是两套逻辑。现在,你通过声明 Capabilities(比如 .video, .pausing, .merging)来告诉系统这个对话能干嘛。系统会根据你声明的能力,自动在锁屏 UI 或灵动岛上渲染对应的按钮(比如长按静音变成暂停)。更妙的是,能力是动态的。语音通话中途升级为视频,只需更新 Capability,系统 UI 会无缝切换。这种声明式 UI 渲染的思路,明显是受了 SwiftUI 的影响,把“控制 UI”变成了“声明状态”。
代码片段
1. 配置并初始化 ConversationManager
场景:在 App 启动时尽早初始化,配置铃声、图标和群组限制。
import LiveCommunicationKit
// 配置对话管理器的基础属性
let configuration = ConversationManager.Configuration(
ringtoneName: "SampleRingtone.caf", // 自定义铃声
iconTemplateImageData: UIImage(named: "AppIcon")?.pngData(), // 灵动岛和锁屏显示的图标
maximumConversationGroups: 1, // 最大对话组数
maximumConversationsPerConversationGroup: 5, // 每组最大对话数(用于群组通话)
includesConversationInRecents: true, // 是否显示在系统电话历史记录中
supportsVideo: true,
supportedHandleTypes: [.phoneNumber, .emailAddress] // 支持的标识符类型
)
let manager = ConversationManager(configuration: configuration)
manager.delegate = self // 必须在生命周期内保持强引用
坑:iconTemplateImageData 必须是单色模板图(Template Image),传彩色 PNG 会被系统强行渲染成奇怪的色块。
2. 处理 PushKit 唤醒并报告来电
场景:收到 VoIP 推送时,解析 payload 并立刻向系统报告新对话。
import PushKit
func pushRegistry(
_ registry: PKPushRegistry,
didReceiveIncomingVoIPPushWith payload: PKPushPayload,
metadata: PKVoIPPushMetadata
) async {
// 从推送 payload 中解析出呼叫者信息和唯一 ID
guard let (handle, uuid) = parsePayload(payload) else { return }
// 声明当前对话支持的能力:视频、暂停、合并
let capabilities: Conversation.Capabilities = [.video, .pausing, .merging]
let update = Conversation.Update(members: [handle], capabilities: capabilities)
// 必须在 PushKit 回调返回前报告,否则系统会直接杀掉 App
try? await manager.reportNewIncomingConversation(uuid: uuid, update: update)
}
坑:reportNewIncomingConversation 是异步的,但 PushKit 的 delegate 方法有严格的超时限制。如果网络请求或解析耗时过长导致没来得及 report,iOS 会认为你在滥用 VoIP 推送,直接终止进程并可能吊销你的推送证书。
3. 统一处理所有交互 Action
场景:在 Delegate 中集中处理来自系统 UI 和 App 内 UI 的所有操作。
func conversationManager(
_ manager: ConversationManager,
perform action: ConversationAction
) {
// 所有交互都会走到这里,通过 switch 统一分发
switch action {
case let joinAction as JoinConversationAction:
handleJoin(joinAction)
case let endAction as EndConversationAction:
handleEnd(endAction)
case let startAction as StartConversationAction:
handleStart(startAction)
default:
// 遇到不支持的 Action 必须显式 fail,否则系统 UI 会一直转圈
action.fail()
}
}
func handleJoin(_ action: JoinConversationAction) {
// 报告正在连接状态,更新系统 UI
manager.reportConversationEvent(.conversationStartedConnecting(.now), for: action.conversation)
Task {
do {
try await setupMediaStream() // 你的 WebRTC 连接逻辑
manager.reportConversationEvent(.conversationConnected(.now), for: action.conversation)
action.fulfill(dateConnected: .now) // 告诉系统连接成功
} catch {
action.fail() // 连接失败,系统会自动清理 UI
}
}
}
坑:千万别忘了在 default 分支里调用 action.fail()。如果你忽略了某个系统发来的 Action(比如静音),系统 UI 上的静音按钮就会一直处于“加载中”的卡死状态。
最佳实践
已有项目的迁移策略
不要试图在旧 CXProvider 和 LiveCommunicationKit 之间做无缝热切换。建议通过 Feature Flag 控制,在灰度版本中让两套代码并存。迁移的重头戏不是改 API 名字,而是重构你的状态机。把原本散落在 CXProviderDelegate 各个回调里的逻辑,全部抽离成纯粹的函数,然后在新的 ConversationManagerDelegate 的 switch action 中统一调用。
新项目的采用建议
直接上 LiveCommunicationKit。在架构设计时,把 ConversationAction 当作你信令层(Signaling Layer)的入口。App 内的 UI 按钮不要直接调用 WebRTC 的 API,而是构造一个 StartConversationAction 或自定义 Action 丢给 Manager,让 Manager 驱动底层逻辑。这样你的 App 内 UI 和系统 UI 就天然保持了行为一致。
实战中容易踩的坑
后台模式配置。必须在 Xcode 的 Background Modes 中同时勾选 Audio, AirPlay, and Picture in Picture 和 Voice over IP。漏掉任何一个,App 切到后台后音频流就会在几秒钟内被系统掐断。另外,Handle 的 value 尽量用真实的手机号或邮箱,这样系统才能正确匹配通讯录,在锁屏显示联系人头像,而不是显示一个干瘪的 UUID。
还有什么值得关注
- 原生通话记录集成:只要配置
includesConversationInRecents: true,你的 App 通话记录会自动混入系统自带的“电话”App 历史记录中,点击还能直接回拨。 - 群组通话的合并与拆分:通过
.merging能力,系统原生支持将两个独立的 1v1 通话合并成一个多人会议,底层信令的复杂度被系统 UI 接管了一大半。 - 灵动岛多任务无缝切换:通话切到后台时,系统会自动在灵动岛生成 Live Activity,用户可以在不打开 App 的情况下进行静音、挂断等操作,且动画完全由系统接管。