WWDC Quick Look 💓 By SwiftGGTeam
Create live communication experiences

Create live communication experiences

观看原视频

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 展示
  • maximumConversationGroupsmaximumConversationsPerConversationGroup:限制合并会话的层级
  • 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

响应系统动作

所有用户交互都通过 ConversationManagerDelegateperform 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 作为兜底。

关联 Session

  • 121 — Siri 与 iPhone 新特性,了解如何集成 StartCallIntent 让 Siri 直接发起通话
  • 223 — Live Activities,学习如何在通话期间用 Live Activity 展示额外信息
  • 310 — Shortcuts,了解 App Intents 捐赠机制,让通话记录可被系统搜索和快捷指令调用
  • 345 — App Intents 新特性,深入理解 intent 捐赠和 NSUserActivity 的集成方式
  • 369 — Bluetooth,学习如何处理通话中的蓝牙耳机路由切换和音频路径管理

评论

GitHub Issues · utterances