Highlight
Apple 推出 LiveCommunicationKit 框架,让第三方实时通信应用获得与系统电话同等的全屏来电界面、Dynamic Island 多任务展示、Phone 应用 Recents 记录和 Siri 唤起能力,取代传统的 CXProvider API。
核心内容
从 CXProvider 到 LiveCommunicationKit
过去,开发者要在 iOS 上实现 VoIP 通话,主要依赖 CallKit 的 CXProvider API。这个方案能做出基本的系统来电界面,但灵活性有限,对群聊、合并通话、视频通话等现代通信场景的支持不够直观。
LiveCommunicationKit 是一个全新的框架,专门解决这些问题。它把”通话”抽象成更通用的”对话(conversation)“概念,支持一对一音频、视频、群聊、合并会话等多种形态,全部接入系统级 UI。
(00:53)
对话的生命周期
一个对话从创建到结束,经历五个状态:
- idle:刚报告给系统,设备开始响铃
- joining:用户接听,应用正在建立连接
- joined:连接就绪,音视频开始传输
- leaving:用户挂断,应用正在清理资源
- left:清理完成,对话结束
(04:10)
每个状态切换都由应用通过 ConversationManager 主动报告。系统根据状态更新 Lock Screen、Dynamic Island 和 Recents 中的 UI 表现。
统一的状态管理
LiveCommunicationKit 的核心设计是”单一数据源”。无论用户从 Lock Screen 滑动接听、从 Dynamic Island 点击挂断,还是在应用内点击按钮,所有交互都转化为 ConversationAction,通过同一个 ConversationManagerDelegate 回调进入应用。
这意味着应用不需要维护两套状态逻辑。系统 UI 和应用 UI 永远同步。
(06:00)
详细内容
配置 ConversationManager
每个使用 LiveCommunicationKit 的应用都需要一个 ConversationManager 实例。通常在应用启动时创建,伴随整个应用生命周期。
import LiveCommunicationKit
let configuration = ConversationManager.Configuration(
ringtoneName: "SampleRingtone.caf",
iconTemplateImageData: UIImage(named: "SampleIcon")?.pngData(),
maximumConversationGroups: 1,
maximumConversationsPerConversationGroup: 2,
includesConversationInRecents: true,
supportsVideo: true,
supportedHandleTypes: [.phoneNumber, .emailAddress]
)
let manager = ConversationManager(configuration: configuration)
manager.delegate = self
关键点:
ringtoneName:应用 bundle 中的铃声文件,来电时播放iconTemplateImageData:应用图标模板,用于系统 UI 展示maximumConversationGroups和maximumConversationsPerConversationGroup:限制合并会话的层级includesConversationInRecents:是否让通话记录出现在 Phone 应用的 Recents 中supportsVideo:启用系统 UI 中的视频按钮supportedHandleTypes:应用支持的联系方式类型,影响联系人匹配
(06:41)
处理来电
来电通过 PushKit 的 VoIP push 唤醒应用。应用必须在 delegate 方法返回前报告对话,否则系统会终止应用。
import LiveCommunicationKit
import PushKit
final class SamplePushHandler: NSObject, PKPushRegistryDelegate {
func pushRegistry(
_ registry: PKPushRegistry,
didReceiveIncomingVoIPPushWith payload: PKPushPayload,
metadata: PKVoIPPushMetadata) async {
guard let (handle, uuid) = parseConversationPayload(from: payload) else { return }
let capabilities = [.video, .pausing, .merging]
let update = Conversation.Update(members: [handle], capabilities: capabilities)
try? await manager.reportNewIncomingConversation(uuid: uuid, update: update)
}
}
关键点:
parseConversationPayload:从 push payload 中提取 handle(来电者标识)和 UUID(对话唯一标识)Conversation.Update:描述对话的初始状态,包含成员和能力capabilities数组声明了对话支持的功能,系统据此决定显示哪些控件reportNewIncomingConversation:必须在方法返回前完成,否则应用被终止
(09:22)
响应系统动作
所有用户交互都通过 ConversationManagerDelegate 的 perform action 方法进入应用。
import LiveCommunicationKit
final class SampleDelegate: ConversationManagerDelegate {
func conversationManager(
_ manager: ConversationManager,
perform action: ConversationAction
) {
switch action {
case let action as JoinConversationAction:
handleJoinAction(action)
case let action as EndConversationAction:
handleEndAction(action)
case let action as StartConversationAction:
handleStartAction(action)
case let action as MergeConversationAction:
handleMergeAction(action)
default:
action.fail()
}
}
}
关键点:
JoinConversationAction:用户接听来电EndConversationAction:用户挂断StartConversationAction:用户从应用内发起通话MergeConversationAction:用户合并两个会话- 未识别的 action 必须调用
fail(),让系统清理 UI 状态
(09:57)
处理接听动作
extension SampleDelegate {
func handleJoinAction(_ action: JoinConversationAction) {
guard let conversation = manager.conversations.first(where: { $0.uuid == action.conversationUUID }) else {
return action.fail()
}
manager.reportConversationEvent(.conversationStartedConnecting(.now), for: conversation)
Task {
do {
try await setupMediaStream(with: action.conversationUUID)
manager.reportConversationEvent(.conversationConnected(.now), for: conversation)
action.fulfill(dateConnected: .now)
} catch {
action.fail()
}
}
}
}
关键点:
- 先通过 UUID 查找对应的 conversation,找不到就
fail() conversationStartedConnecting:通知系统进入 joining 状态,UI 显示”连接中”setupMediaStream:应用自己的媒体连接逻辑,运行在 Task 中保持 delegate 响应conversationConnected:媒体就绪后通知系统进入 joined 状态fulfill(dateConnected:):标记 action 完成,系统更新 UI- 任何异常都走
fail(),系统会自动清理
(10:13)
发起去电
从应用内发起通话时,创建 StartConversationAction 并通过 ConversationManager 执行。
let startAction = StartConversationAction(
conversationUUID: UUID(),
handles: [Handle(type: .phoneNumber, value: "+1-650-555-0199", displayName: "Ryan Notch")],
isVideo: false
)
try await manager.perform([startAction])
关键点:
conversationUUID:新生成的唯一标识handles:接收方的联系方式数组isVideo:是否为视频通话manager.perform:将 action 提交给系统,系统更新 UI 后转发给 delegate 处理
(12:14)
群聊与会话合并
群聊需要区分两种成员列表:
members:所有被邀请的参与者activeRemoteMembers:当前正在发送媒体的参与者
let adam = Handle(type: .emailAddress,
value: "[email protected]",
displayName: "Adam Halwani")
let david = Handle(type: .emailAddress,
value: "[email protected]",
displayName: "David Evans")
let ryan = Handle(type: .phoneNumber,
value: "+16505550199",
displayName: "Ryan Notch")
let startAction = StartConversationAction(
conversationUUID: UUID(),
handles: [david, ryan],
isVideo: false
)
try await manager.perform([startAction])
成员变化时报告更新:
let update = Conversation.Update(
localMember: adam,
members: [david, ryan],
activeRemoteMembers: [david, ryan],
capabilities: [.merging, .pausing, .unmerging]
)
manager.reportConversationEvent(
.conversationUpdated(update),
for: conversation
)
(13:51)
合并两个会话
当用户把两个并行会话合并成一个群聊时,系统发送 MergeConversationAction。
extension SampleDelegate {
func handleMergeAction(_ action: MergeConversationAction) {
let sourceUUID = action.conversationUUID
let targetUUID = action.conversationUUIDToMergeWith
guard manager.conversations.contains(where: { $0.uuid == sourceUUID }),
manager.conversations.contains(where: { $0.uuid == targetUUID }) else {
return action.fail()
}
Task {
do {
let update = try await combineStreams(from: sourceUUID, into: targetUUID)
manager.reportConversationEvent(.conversationUpdated(update), for: target)
action.fulfill()
} catch {
action.fail()
}
}
}
}
关键点:
conversationUUID:源会话的标识conversationUUIDToMergeWith:目标会话的标识- 合并前验证两个会话都存在
combineStreams:应用自己的服务端逻辑,把两个媒体流合并- 合并完成后报告更新并
fulfill() - 解合并(unmerging)遵循完全相同的委托模式
(15:33)
核心启发
1. 把任何实时互动应用做成”系统级通话”
在线客服、远程协作、游戏语音都可以接入 LiveCommunicationKit。用户从 Lock Screen 直接接听,从 Dynamic Island 查看状态,体验与打电话一致。入口 API 是 ConversationManager + reportNewIncomingConversation。
2. 用群聊合并实现”拉人进群”
客服场景里,一线客服可以先与用户一对一沟通,需要技术支持时合并进专家会话,不需要用户挂断重拨。实现入口是声明 .merging capability,处理 MergeConversationAction。
3. 让通话记录成为应用入口
开启 includesConversationInRecents: true 后,用户从 Phone 应用的 Recents 直接回拨。配合 StartCallIntent 捐赠,Siri 也能直接唤起通话。这是降低用户流失的自然流量入口。
4. 动态切换音视频
Capabilities 可以在对话生命周期中动态更新。音频通话中途打开摄像头,只需更新 capabilities 加入 .video,系统 UI 立即响应。不需要重建整个对话。
5. 用 Handle 打通联系人系统
把应用内的用户标识映射为 .phoneNumber 或 .emailAddress 类型的 Handle,系统会自动匹配通讯录中的联系人头像和姓名。陌生号码显示 displayName 作为兜底。
评论
GitHub Issues · utterances