接听第一通电话

最近更新时间:2026-02-04 15:40:01

我的收藏
本文档将帮助您使用 AtomicXCore SDKDeviceStoreCallStore 以及核心组件 CallCoreView,快速完成接听电话功能。


核心功能

AtomicXCore 中用于搭建多人音视频通话场景所需要使用到的核心模块包含以下三个:
模块
功能描述
通话视图核心组件。自动监听 CallStore 数据并完成画面渲染,同时提供布局切换、头像与图标配置等 UI 定制化能力。
CallStore
通话生命周期管理:拨打电话、接通电话、拒接电话、挂断电话。实时获取参与通话人员音视频状态,通话计时、通话记录等数据。
音视频设备控制:麦克风(开关 / 音量)、摄像头(开关 / 切换 / 画质)、屏幕共享,设备状态实时监听。

准备工作

步骤1:开通服务

请参见 开通服务,获取体验版或付费版 SDK。

步骤2:集成 SDK

1. 添加 Pod 依赖:在您项目的 Podfile 文件中添加 pod 'AtomicXCore' 依赖。
target 'YourProjectTarget' do
pod 'AtomicXCore'

end
提示:
若项目没有 Podfile 文件:在终端中进入到您的 .xcodeproj 目录下,然后执行 pod init 命令创建 Podfile 文件
2. 安装组件:在终端进入到 Podfile 文件所在的目录,然后执行以下命令安装组件。
pod install --repo-update
提示:
安装完成后,使用 YourProjectName.xcworkspace 文件打开项目

步骤3:初始化与登录流程

启动通话服务需依次完成 CallStore 初始化与用户登录。CallStore 通过监听登录成功事件自动同步用户信息,从而进入就绪状态。流程图与示例代码如下:

iOS(Swift)
import UIKit
import AtomicXCore
import Combine

class ViewController: UIViewController {
var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// CallStore 初始化
let _ = CallStore.shared
// 配置您的信息
let userID = "test_001" // 请替换为您的 UserID
let sdkAppID: Int = 1400000001 // 请替换为在控制台得到的 SDKAppID
let secretKey = "**************" // 请替换为在控制台得到的 SecretKey
// 生成 UserSig(仅用于本地快速测试,正式环境请在服务端生成)
let userSig = GenerateTestUserSig.genTestUserSig(
userID: userID,
sdkAppID: sdkAppID,
secretKey: secretKey
)
// 登录
LoginStore.shared.login(
sdkAppID: sdkAppID,
userID: userID,
userSig: userSig
) { result in
switch result {
case .success:
// 登录成功处理
Log.info("login success")
case .failure(let error):
// 登录失败处理
Log.error("login failed, code: \\(error.code), error: \\(error.message)")
}
}
}
}

参数
类型
说明
userID
String
当前用户的唯一 ID,仅包含英文字母、数字、连字符和下划线。为避免多端登录冲突,请勿使用 1、123 等简单 ID。
sdkAppID
int
控制台 获取,通常是以 140 或 160 开头的 10 位整数。
secretKey
String
控制台 创建的音视频应用的 SDKSecretKey。
userSig
String
用于腾讯云鉴权的票据。请注意:
开发环境:您可以采用本地 GenerateTestUserSig.genTestUserSig 函数生成 userSig 或者 通过 UserSig 辅助工具 生成临时的 UserSig。
生产环境:为了防止密钥泄露,请务必采用服务端生成 UserSig 的方式。详细信息请参考 服务端生成 UserSig
更多信息请参见 如何计算及使用 UserSig

实现接听通话

接听通话前,请确保已完成登录,这是服务可用的必要前提。接下来,我们将通过 6 个步骤带您实现‘接听一通电话’的功能。

步骤1: 创建通话页面

您需要创建一个通话页面,当发起通话唤起通话页面,实现方式如下:
1. 创建通话页面:您可以新建一个 UIViewController 作为通话宿主页面,用于响应来电时的跳转逻辑。
2. 通话页面绑定 CallCoreView : 通话视图核心组件,自动监听 CallStore 数据并完成画面渲染,同时提供布局切换、头像与图标配置等 UI 定制化能力。
import UIKit
import AtomicXCore

class CallViewController: UIViewController {
// 1.创建通话页面
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
//2.通话页面绑定 CallCoreView
callCoreView = CallCoreView(frame: view.bounds)
callCoreView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
if let callCoreView = callCoreView {
view.addSubview(callCoreView)
}
}
}
CallCoreView 视图组件功能说明:
功能
说明
参考文档
设置布局模式
支持自由切换布局模式。若未设置,将根据通话人数自动适配布局。
设置头像
支持通过传入头像资源路径,为特定用户自定义头像。
设置音量提示图标
支持根据不同音量等级,配置个性化的音量指示图标。
设置网络提示图标
支持根据实时网络质量,配置对应的网络状态提示图标。
设置等待接听用户的动画
在多人通话场景下,支持传入 GIF 图像路径,为待接听状态的用户展示动画。

步骤2:添加接听和拒接按钮

您可以参考 DeviceStoreCallStore 提供的 API ,自定义添加您的按钮。
DeviceStore 功能说明:麦克风(开关 / 音量)、摄像头(开关 / 切换 / 画质)、屏幕共享,设备状态实时监听。建议将对应方法绑定至按钮点击事件,并通过监听设备状态变更来实时刷新按钮的 UI 状态。
CallStore 功能说明:接听、挂断、拒接等核心通话控制能力。建议将对应方法绑定至按钮点击事件,并监听通话状态的变化,以确保按钮显示与当前通话阶段保持同步。
图标资源下载:按钮图标可以直接从 GitHub 下载。这些图标由我们的设计师专为 TUICallKit 打造,无版权风险,可放心使用。
图标:






















下载地址:
以下是添加"接听"和"拒接"按钮的实现方式:
1.1 添加接听和拒接按钮:添加"接听"与"拒接"按钮,将其点击事件分别绑定至 acceptreject 方法。
iOS(Swift)
import UIKit
import AtomicXCore

class CallViewController: UIViewController {
private var buttonReject: UIButton?
private var buttonAccept: UIButton?
// 1.创建页面容器
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
// 其他初始化代码
// 1.添加接听和拒接按钮
addControlButtons()
}
private func addControlButtons() {
let buttonWidth: CGFloat = 80
let buttonHeight: CGFloat = 80
let spacing: CGFloat = 60
let bottomMargin: CGFloat = 100
let totalWidth = buttonWidth * 2 + spacing
let startX = (view.bounds.width - totalWidth) / 2
let buttonY = view.bounds.height - bottomMargin - buttonHeight
// 创建接听按钮
buttonAccept = createButton(
frame: CGRect(x: startX, y: buttonY, width: buttonWidth, height: buttonHeight),
title: "接听",
backgroundColor: .systemGreen
)
buttonAccept?.addTarget(self, action: #selector(touchAcceptButton), for: .touchUpInside)
view.addSubview(buttonAccept!)
// 创建拒接按钮
buttonReject = createButton(
frame: CGRect(x: startX + buttonWidth + spacing, y: buttonY, width: buttonWidth, height: buttonHeight),
title: "拒接",
backgroundColor: .systemRed
)
buttonReject?.addTarget(self, action: #selector(touchRejectButton), for: .touchUpInside)
view.addSubview(buttonReject!)
}
// 2.接听按钮点击事件绑定 accept
@objc private func touchAcceptButton() {
CallStore.shared.accept(completion: nil)
}
// 2.拒接按钮点击事件绑定 reject
@objc private func touchRejectButton() {
CallStore.shared.reject(completion: nil)
}
// 创建圆形按钮的辅助方法
private func createButton(frame: CGRect, title: String, backgroundColor: UIColor) -> UIButton {
let button = UIButton(type: .system)
button.frame = frame
button.setTitle(title, for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = backgroundColor
button.layer.cornerRadius = frame.width / 2
button.titleLabel?.font = UIFont.systemFont(ofSize: 16)
return button
}
}

1.2 拨打方取消通话或您拒接时销毁界面:无论拨打方取消呼叫,还是接收方拒接,均会触发 onCallEnded(通话结束)事件。建议监听此事件,以便在通话终止时及时关闭(销毁)通话界面。
import UIKit
import AtomicXCore
import Combine

class CallViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// ... 其他初始化代码
// 1.通话事件监听
addListener()
}
private func addListener() {
CallStore.shared.callEventPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
if case .onCallEnded = event {
// 2.通话结束关闭页面
self?.dismiss(animated: true)
}
}
.store(in: &cancellables)
}
}

步骤3:申请音视频权限

建议在呼叫接通前,先行检测音视频权限。若权限缺失,请引导用户动态申请。具体方法如下:
1. 声明权限:首先,在 Info.plist 文件中添加以下两项,并填写对应的使用说明,这些说明将在系统请求权限时向用户显示。
<key>NSCameraUsageDescription</key>
<string>需要访问您的摄像头,并可用于视频通话、群组视频通话等功能</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要访问您的麦克风,并可用于音频通话、群组音频通话、视频通话、群组视频通话等功能</string>
2. 动态权限申请:建议在收到来电时,或结合具体业务场景(如点击呼叫前),按需发起音视频权限的申请。
import AVFoundation
import UIKit

extension UIViewController {
// 检查麦克风权限
func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
let status = AVCaptureDevice.authorizationStatus(for: .audio)
switch status {
case .authorized:
completion(true)
case .notDetermined:
AVCaptureDevice.requestAccess(for: .audio) { granted in
DispatchQueue.main.async {
completion(granted)
}
}
case .denied, .restricted:
completion(false)
@unknown default:
completion(false)
}
}
// 检查摄像头权限
func checkCameraPermission(completion: @escaping (Bool) -> Void) {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
completion(true)
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
completion(granted)
}
}
case .denied, .restricted:
completion(false)
@unknown default:
completion(false)
}
}
// 显示权限提示
func showPermissionAlert(message: String) {
let alert = UIAlertController(
title: "权限请求",
message: message,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
}
}


步骤4:来电播放提示

您可以监听当前用户的通话状态,在收到来电时播放铃声或振动,接听、挂断后停止播放来电提示,实现方法如下:
1. 数据层订阅:订阅 CallStore.shared.state.value.selfInfo , 建立当前登录用户信息的响应式监听。
2. 播放/停止铃声或振动:若当前用户的通话状态 (selfInfo.status) 为等待接听状态(.waiting)播放铃声或振动,若当前用户的通话状态 (selfInfo.status) 为已接听状态(.accept)停止铃声或振动。
import UIKit
import AtomicXCore
import Combine
import AVFoundation

class MainViewController: UIViewController {
private var audioPlayer: AVAudioPlayer?
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 1.监听当前用户的通话状态
observeSelfStatus()
}
// 监听当前用户的通话状态
private func observeSelfStatus() {
CallStore.shared.state
.subscribe(StatePublisherSelector<CallState, CallParticipantStatus>(keyPath: \\.selfInfo.status))
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] status in // 2.播放/停止来电提示
self?.handleRingtoneByStatus(status)
}
.store(in: &cancellables)
}
// 根据当前用户通话状态播放来电提示
private func handleRingtoneByStatus(_ status: CallParticipantStatus) {
switch status {
case .waiting:
// 等待接听时开始播放铃声
// playRingtone()
case .accept:
// 接听后停止播放铃声
// stopRingtone()
default:
// 其他状态停止播放铃声
// stopRingtone()
}
}
}

步骤5:来电打开媒体设备

收到来电时,您可以通过 onCallReceived 事件获取本次通话的媒体类型。为了提供更佳的用户体验,建议在唤起通话界面时,根据通话类型预先开启对应的媒体设备。实现步骤如下:
1. 监听来电事件:订阅 onCallReceived 事件。
2. 根据来电媒体类型打开设备:若为语音通话仅开启麦克风,若为视频通话开启麦克风和摄像头。
import UIKit
import AtomicXCore
import Combine

class MainViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// ... 其他初始化代码
// 1.监听来电事件
addListener()
}
// 监听来电事件
private func addListener() {
CallStore.shared.callEventPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
if case .onCallReceived(let callId, let mediaType, let userData) = event {
// 2.根据来电媒体类型打开设备
self?.openDeviceForMediaType(mediaType)
}
}
.store(in: &cancellables)
}
// 根据通话媒体类型打开设备
private func openDeviceForMediaType(_ mediaType: CallMediaType) {
DeviceStore.shared.openLocalMicrophone(completion: nil)
if mediaType == .video {
let isFront = true
DeviceStore.shared.openLocalCamera(isFront: isFront, completion: nil)
}
}
}
openLocalCamera 接口参数详细说明:
参数名
类型
必填
说明
isFront
Bool
是否开启前置摄像头。
true : 开启前置摄像头。
false :开启后置摄像头。
completion
CompletionHandler
操作完成回调,用于返回开启摄像头的结果。若开启失败则会返回错误码和错误信息。
openLocalMicrophone 接口参数详细说明:
参数名
类型
必填
说明
completion
CompletionHandler
操作完成回调,用于返回开启麦克风的结果。若开启失败则会返回错误码和错误信息。

步骤6:来电唤起通话界面

在您的项目中监听来电事件(onCallReceived),当收到来电时唤起通话页面。实现方式如下:
1. 监听来电事件:订阅 onCallReceived 事件。
2. 唤起通话界面:收到来电唤起通话页面。
import UIKit
import AtomicXCore
import Combine

class MainViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// ... 其他初始化代码
// 1.监听来电事件
addListener()
}
// 监听来电事件
private func addListener() {
CallStore.shared.callEventPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] event in
if case .onCallReceived(let callId, let mediaType, let userData) = event {
// 2.唤起通话页面
let callVC = CallViewController()
callVC.modalPresentationStyle = .fullScreen
self?.present(callVC, animated: true)
}
}
.store(in: &cancellables)
}
}

onCallReceived 事件详细说明:
参数
类型
说明
callId
String
此次通话的唯一标识。
mediaType
通话媒体类型,用于指定发起音频通话还是视频通话。
CallMediaType.video : 视频通话。
CallMediaType.audio : 语音通话。

运行效果

当您完成以上 6 步后,"接听一通电话"运行效果如下:


定制页面

CallCoreView 提供了完善的 UI 定制能力,支持头像及音量提示等图标的自由替换。为助力快速集成,您可以直接从 GitHub 下载。无版权风险,可放心使用。

自定义音量提示的图标

您可以调用 CallCoreView 组件的 setVolumeLevelIcons 设置音量大小等级不同的提示图标。

setVolumeLevelIcons 示例代码:
// 设置音量提示图标
let volumeLevelIcons: [VolumeLevel: String] = [
.mute: "对应图标资源的路径"
]
callCoreView.setVolumeLevelIcons(icons: volumeLevelIcons)
setVolumeLevelIcons 接口参数详细说明:
参数
类型
是否必填
说明
icons
[VolumeLevel: String]
【字段含义】音量等级与图标资源的映射表。字典结构说明如下:
key ( VolumeLevel ) 表示音量等级:
VolumeLevel.mute :表示麦克风关闭,静音状态。
VolumeLevel.low :表示音量范围 (0 ~ 25]。
VolumeLevel.medium : 表示音量范围 (25 ~ 50]。
VolumeLevel.high : 表示音量范围在 (50 ~ 75]
VolumeLevel.peak : 表示音量范围在 (75 ~ 100]。
Value ( String ) 表示对应音量等级的图标资源路径。
音量提示图标:
图标
说明
下载地址

【图标含义】音量提示图标。
【推荐用法】您可以将该图标等级设置为 VolumeLevel.lowVolumeLevel.medium ,当用户音量大于对应等级时显示。

【图标含义】静音图标。
【推荐用法】您可以将该图标等级设置为 VolumeLevel.mute ,当该用户静音时显示。

自定义网络提示的图标

您可以调用 CallCoreView 组件的 setNetworkQualityIcons 设置不同网络状态的提示图标。

setNetworkQualityIcons 示例代码:
// 设置网络质量图标
let networkQualityIcons: [NetworkQuality: String] = [
.bad: "对应图标的路径"
]
callCoreView.setNetworkQualityIcons(icons: networkQualityIcons)
setNetworkQualityIcons 接口参数详细说明:
参数
类型
是否必填
说明
icons
[NetworkQuality: String]
网络质量与图标资源的映射表。字典结构说明如下:
Key ( NetworkQuality ) : 表示网络质量等级
NetworkQuality.unknown :未知网络状态。
NetworkQuality.excellent:网络状态极佳。
NetworkQuality.good : 网络状态较好。
NetworkQuality.poor : 网络状态较差。
NetworkQuality.bad : 网络状态差。
NetworkQuality.veryBad :网络状态极差。
NetworkQuality.down :网络断开。
Value ( String ) : 对应网络状态的图标资源路径。
网络较差的提示图标:
图标
说明
下载地址

【图标含义】网络较差的提示图标。
【推荐用法】您可以将该图标等级设置为 NetworkQuality.badNetworkQuality.veryBadNetworkQuality.down ,当网络较差时显示该图标。

自定义默认头像

您可以调用 CallCoreViewsetParticipantAvatars 接口设置用户头像。建议您监听响应式数据 allParticipants(所有参与通话的成员):当获取到用户头像时设置并展示;若用户未设置头像或加载失败,则显示默认头像(占位图)。
setParticipantAvatars 示例代码:
// 设置用户头像
var avatars: [String: String] = [:]
let userId = "" // 用户 ID
let avatarPath = "" // 用户默认头像资源的路径
avatars[userId] = avatarPath

callCoreView.setParticipantAvatars(avatars: avatars)
setParticipantAvatars 接口参数详细说明:
参数
类型
是否必填
说明
avatars
[String: String]
用户头像映射表。
Key : 用户的 userID 。
Value : 该用户的头像资源绝对路径。
默认头像资源:
图标
说明
下载地址

【图标含义】默认头像
【推荐用法】当用户头像加载失败或无头像时,您可以给该用户设置此默认头像。

自定义 loading 动画

您可以调用 CallCoreView 的 setWaitingAnimation 接口,为等待中用户设置等待动画获得更好的体验。

setWaitingAnimation 示例代码:
iOS
// 设置等待动画
let waitingAnimationPath = "" // 等待动画 GIF 图像资源的路径
callCoreView.setWaitingAnimation(path: waitingAnimationPath)
setWaitingAnimation 接口参数详细说明:
参数
类型
是否必填
说明
path
String
GIF 格式图像资源的绝对路径。
等待接听的动画:
图标
说明
下载地址

【图标含义】用户等待接听动画。
【推荐用法】群组通话时设置的动画。设置后,当用户的状态为等待接听时,显示该动画。

添加通话计时提示

通话计时可通过响应式数据 activeCallduration 字段实时获得,实时显示通话计时的实现方式如下:
1. 数据层订阅:订阅 CallStore.observerState.activeCall , 建立当前活跃通话的响应式监听。
2. 绑定通话计时数据:将 activeCall.duration 字段绑定至 UI 控件。该字段为响应式数据,会自动驱动 UI 实时刷新,无需手动维护定时器。
import UIKit
import AtomicXCore
import Combine

class TimerView: UILabel {
private var cancellables = Set<AnyCancellable>()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
private func setupView() {
textColor = .white
textAlignment = .center
font = .systemFont(ofSize: 16)
}
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
registerActiveCallObserver()
} else {
cancellables.removeAll()
}
}
private func registerActiveCallObserver() {
CallStore.shared.state.subscribe()
.map { $0.activeCall }
.removeDuplicates { $0.duration == $1.duration }
.receive(on: DispatchQueue.main)
.sink { [weak self] activeCall in
// 更新通话计时
self?.updateDurationView(activeCall: activeCall)
}
.store(in: &cancellables)
}
private func updateDurationView(activeCall: CallInfo) {
let currentDuration = activeCall.duration
let minutes = currentDuration / 60
let seconds = currentDuration % 60
text = String(format: "%02d:%02d", minutes, seconds)
}
}
说明:
若您想了解更多通话状态响应式数据,详细可参考:CallState

更多功能

自定义用户头像/昵称

通话开始前,您可以通过 setSelfInfo 方法,设置自己的昵称和头像。
setSelfInfo 示例代码
import AtomicXCore

var userProfile = UserProfile()
userProfile.userID = "" // 您的 userId
userProfile.avatarURL = "" // 头像的 url
userProfile.nickname = "" // 需要设置的昵称

LoginStore.shared.setSelfInfo(userProfile: userProfile) { result in
switch result {
case .success:
// 设置成功回调
case .failure(let error):
// 设置失败回调
}
}
setSelfInfo 接口参数详细说明:
参数
类型
是否必填
说明
userProfile
用户信息结构体:
userID :用户的 ID。
avatarURL : 用户头像的 URL。
nickname :用户的昵称。
更多字段详情可参考 UserProfile
completion
CompletionHandler
【字段含义】操作完成回调,用于返回接通电话的结果。

切换布局模式

CallCoreView 内置三种布局模式,您可以调用 setLayoutTemplate 设置布局模式。若未主动配置,CallCoreView 将根据通话人数自动适配:1 V 1 场景下默认采用 Float 模式,多人通话场景下则自动切换为 Grid 模式。不同布局模式的说明如下:
Float 模式
Grid 模式
PIP 模式



布局逻辑:呼叫等待时全屏显示己方画面;接通后全屏显示对方画面,己方画面以悬浮小窗展示。
交互特性:支持小窗拖拽移动,点击小窗可实现大小画面互换。
布局逻辑:所有成员画面呈网格状平铺排列成宫格模式布局,适用 2 人以上通话,支持点击放大画面功能。
交互特性:支持点击特定成员画面放大查看。
布局逻辑:1v1 场景固定显示对方画面,多人场景:采用当前发言者(Active Speaker) 策略,自动识别并全屏展示正在说话的用户。
交互特性:等待时显示自己的画面,接通后还会显示通话计时。
setLayoutTemplate 示例代码:
// 创建 CallCoreView
let callCoreView = CallCoreView(frame: view.bounds)
callCoreView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

// 设置布局模式
let template = CallLayoutTemplate.grid
callCoreView.setLayoutTemplate(template)
setLayoutTemplate 接口参数详细说明:
参数
类型
是否必填
说明
template
【字段含义】CallCoreView 的布局模式
CallLayoutTemplate.float
布局逻辑:呼叫等待时全屏显示己方画面;接通后全屏显示对方画面,己方画面以悬浮小窗展示。
交互特性:支持小窗拖拽移动,点击小窗可实现大小画面互换。
CallLayoutTemplate.grid
布局逻辑:所有成员画面呈网格状平铺排列成宫格模式布局,适用 2 人以上通话,支持点击放大画面功能。
交互特性:支持点击特定成员画面放大查看。
CallLayoutTemplate.pip :
布局逻辑:1v1 场景固定显示对方画面,多人场景:采用当前发言者(Active Speaker) 策略,自动识别并全屏展示正在说话的用户。
交互特性:等待时显示自己的画面,接通后还会显示通话计时。

实现应用内悬浮窗

AtomicXCore SDK 提供了 CallPipView 组件,可以在应用内实现悬浮窗效果。当您的通话界面被其他页面覆盖时(例如用户点击了返回键但通话未结束),可以显示一个悬浮的小窗口,让用户随时查看通话状态并快速返回通话界面。
第一步:创建悬浮窗控制器。
import UIKit
import AtomicXCore
import Combine

/**
* 悬浮窗控制器
*
* 用于显示通话悬浮窗,内部包含 CallCoreView
*/
class FloatWindowViewController: UIViewController {
var tapGestureAction: (() -> Void)?
private var cancellables = Set<AnyCancellable>()
private lazy var callCoreView: CallCoreView = {
let view = CallCoreView(frame: self.view.bounds)
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.setLayoutTemplate(.pip) // 设置为 Pip 布局模式
view.isUserInteractionEnabled = false // 禁用交互,让点击事件穿透到父视图
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.1, alpha: 1.0)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
view.addSubview(callCoreView)
// 添加点击手势
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapGesture)
// 延迟监听状态变化,避免在创建时立即被关闭
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.observeCallStatus()
}
}
@objc private func handleTap() {
tapGestureAction?()
}
/**
* 监听通话状态变化
* 当通话结束时自动关闭悬浮窗
*/
private func observeCallStatus() {
CallStore.shared.state
.subscribe(StatePublisherSelector<CallState, CallParticipantStatus>(keyPath: \\.selfInfo.status))
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] status in
if status == .none {
// 通话结束,发送通知隐藏悬浮窗
NotificationCenter.default.post(name: NSNotification.Name("HideFloatingWindow"), object: nil)
}
}
.store(in: &cancellables)
}
deinit {
cancellables.removeAll()
}
}
第二步:在主界面实现悬浮窗管理逻辑。

import UIKit
import AtomicXCore

class MainViewController: UIViewController {
private var floatWindow: UIWindow?
override func viewDidLoad() {
super.viewDidLoad()
// 监听显示悬浮窗的通知
NotificationCenter.default.addObserver(
self,
selector: #selector(showFloatingWindow),
name: NSNotification.Name("ShowFloatingWindow"),
object: nil
)
// 监听隐藏悬浮窗的通知
NotificationCenter.default.addObserver(
self,
selector: #selector(hideFloatingWindow),
name: NSNotification.Name("HideFloatingWindow"),
object: nil
)
}
/**
* 显示应用内悬浮窗
*/
@objc private func showFloatingWindow() {
// 检查通话是否进行中
let selfStatus = CallStore.shared.state.value.selfInfo.status
guard selfStatus == .accept else {
return
}
// 如果悬浮窗已存在,不重复创建
guard floatWindow == nil else { return }
// ⚠️ 关键:必须使用当前的 windowScene 来创建 window
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
return
}
// 创建悬浮窗(宽高比 9:16)
let pipWidth: CGFloat = 100
let pipHeight: CGFloat = pipWidth * 16 / 9
let pipX = UIScreen.main.bounds.width - pipWidth - 20
let pipY: CGFloat = 100
// 创建悬浮窗 Window(关联到 windowScene)
let window = UIWindow(windowScene: windowScene)
window.windowLevel = .alert + 1
window.backgroundColor = .clear
window.frame = CGRect(x: pipX, y: pipY, width: pipWidth, height: pipHeight)
// 创建悬浮窗控制器
let floatVC = FloatWindowViewController()
floatVC.tapGestureAction = { [weak self] in
self?.openCallViewController()
}
window.rootViewController = floatVC
self.floatWindow = window
// 设置可见
window.isHidden = false
window.makeKeyAndVisible()
// 立即将主窗口恢复为 key window
if let mainWindow = windowScene.windows.first(where: { $0 != window }) {
mainWindow.makeKey()
}
}
/**
* 隐藏应用内悬浮窗
*/
@objc private func hideFloatingWindow() {
floatWindow?.isHidden = true
floatWindow = nil
}
/**
* 打开通话界面(从悬浮窗点击进入)
*/
private func openCallViewController() {
// 先隐藏悬浮窗
hideFloatingWindow()
// 获取当前最顶层的 ViewController
guard let topVC = getTopViewController() else {
return
}
let callVC = CallViewController()
callVC.modalPresentationStyle = .fullScreen
topVC.present(callVC, animated: true)
}
/**
* 获取当前最顶层的 ViewController
*/
private func getTopViewController() -> UIViewController? {
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }),
let rootVC = keyWindow.rootViewController else {
return nil
}
var topVC = rootVC
while let presentedVC = topVC.presentedViewController {
topVC = presentedVC
}
return topVC
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
第三步:在通话界面添加悬浮窗触发逻辑。
import UIKit
import AtomicXCore

class CallViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 进入通话界面时,发送通知隐藏悬浮窗
NotificationCenter.default.post(name: NSNotification.Name("HideFloatingWindow"), object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 离开通话界面时,检查通话是否进行中
let selfStatus = CallStore.shared.state.value.selfInfo.status
if selfStatus == .accept {
// 如果通话还在进行中,发送通知显示悬浮窗
NotificationCenter.default.post(name: NSNotification.Name("ShowFloatingWindow"), object: nil)
}
}
}

实现应用外画中画

AtomicXCore SDK 支持通过底层 TRTC 引擎实现应用外画中画功能。当应用进入后台时,通话画面可以以系统画中画形式悬浮在其他应用之上,用户可以边使用其他应用边进行视频通话。
说明:
1. 需要在 Xcode 的 Signing & Capabilities 中添加 Background Modes 能力,并勾选 Audio, AirPlay, and Picture in Picture。
2. 需要 iOS 15.0 及以上版本支持。
1. 配置画中画参数:您需要设置画中画窗口的填充模式、用户视频区域、画布参数等参数。
import AtomicXCore

// 填充模式枚举
enum PictureInPictureFillMode: Int, Codable {
case fill = 0 // 填充模式
case fit = 1 // 适应模式
}

// 用户视频区域
struct PictureInPictureRegion: Codable {
let userId: String // 用户 ID
let width: Double // 宽度(0.0-1.0,相对于画布)
let height: Double // 高度(0.0-1.0,相对于画布)
let x: Double // X 坐标(0.0-1.0,相对于画布左上角)
let y: Double // Y 坐标(0.0-1.0,相对于画布左上角)
let fillMode: PictureInPictureFillMode // 填充模式
let streamType: String // 流类型("high" 或 "low")
let backgroundColor: String // 背景颜色
}

// 画布配置
struct PictureInPictureCanvas: Codable {
let width: Int // 画布宽度
let height: Int // 画布高度
let backgroundColor: String // 背景颜色
}

// 画中画参数
struct PictureInPictureParams: Codable {
let enable: Bool // 是否启用画中画
let cameraBackgroundCapture: Bool? // 是否在后台采集摄像头
let canvas: PictureInPictureCanvas? // 画布配置(可选)
let regions: [PictureInPictureRegion]? // 用户视频区域列表(可选)
}

// 画中画请求
struct PictureInPictureRequest: Codable {
let api: String // API 名称
let params: PictureInPictureParams // 参数
}
2. 启用画中画:您可以通过 configPictureInPicture 方法启用/关闭画中画功能。
let params = PictureInPictureParams(
enable: true,
cameraBackgroundCapture: true,
canvas: nil,
regions: nil
)

let request = PictureInPictureRequest(
api: "configPictureInPicture",
params: params
)

// 编码为 JSON 字符串
let encoder = JSONEncoder()
if let data = try? encoder.encode(request),
let jsonString = String(data: data, encoding: .utf8) {
TUICallEngine.createInstance().callExperimentalAPI(jsonObject: jsonString)
}

设置通话过程中屏幕常亮

通话过程中保持屏幕常亮是通话应用的基本需求。iOS 提供了 UIApplication.shared.isIdleTimerDisabled 属性来实现此功能。
class CallViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 禁用自动锁屏,保持屏幕常亮
UIApplication.shared.isIdleTimerDisabled = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 恢复自动锁屏
UIApplication.shared.isIdleTimerDisabled = false
}
}

开启后台采集音频/视频常见问题

为了确保应用在进入后台时仍能正常采集音频和视频(例如用户锁屏或切换到其他应用),您需要完成:iOS 后台模式权限配置设置音频会话,实现方式如下:
1. iOS 后台模式权限配置:在 Xcode 中打开您的项目
选择项目的 TargetSigning & Capabilities
点击 + Capability
搜索并添加 Background Modes
勾选以下三个选项:
1.1.1 Audio, AirPlay, and Picture in Picture(保持音频采集和画中画功能)。
1.1.2 Voice over IP(支持 VoIP 通话)。
1.1.3 Remote notifications(可选,用于接收离线推送)。
配置完成后,您的 Info.plist 文件会自动添加以下内容:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>voip</string>
<string>remote-notification</string>
</array>
2. 配置音频会话(AVAudioSession):为了确保通话音频在后台正常工作,您需要在通话开始前配置音频会话。建议在通话界面的 viewDidLoad 或发起通话前设置:
import AVFoundation

/**
* 设置音频会话,支持后台音频采集
*
* 建议在以下场景调用:
* 1. 通话界面的 viewDidLoad 中
* 2. 发起通话 (calls) 之前
* 3. 接听通话 (accept) 之前
*/
private func setupAudioSession() {
let audioSession = AVAudioSession.sharedInstance()
do {
// 设置音频会话类别为播放和录音
// .allowBluetooth: 支持蓝牙耳机
// .allowBluetoothA2DP: 支持高质量蓝牙音频(A2DP 协议)
try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP])
// 激活音频会话
try audioSession.setActive(true)
} catch {
// 音频会话配置失败
}
}
3. 播放铃声时的特殊处理(可选):如果您需要在等待接听时播放铃声,并且希望铃声通过扬声器播放,可以临时切换音频会话为 .playback 模式:
/**
* 播放铃声时切换音频会话
*
* 使用场景:当您使用 AVAudioPlayer 播放铃声时
*/
private func setAudioSessionForRingtone() {
let audioSession = AVAudioSession.sharedInstance()
do {
// 切换为播放模式
try audioSession.setCategory(.playback, options: [.allowBluetooth, .allowBluetoothA2DP])
// 强制使用扬声器播放铃声
try audioSession.overrideOutputAudioPort(.speaker)
try audioSession.setActive(true)
} catch {
// 铃声音频会话配置失败
}
}

/**
* 停止播放铃声后恢复为通话模式
*/
private func restoreAudioSessionForCall() {
let audioSession = AVAudioSession.sharedInstance()
do {
// 恢复为播放和录音模式
try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP])
try audioSession.setActive(true)
} catch {
// 恢复音频会话失败
}
}

下一步

恭喜您,已经完成了“接听一通电话”,接下来,您可参考 拨打第一通电话 实现拨打电话功能。

常见问题

在通话邀请超时时间内,被邀请者如果离线再上线,能否收到来电事件?

单人通话时,如果在超时时间内上线,会触发来电邀请;群组通话,如果在超时时间内上线,会拉起未处理的20条群消息,如果存在通话邀请,则触发来电邀请事件。

联系我们

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