拨打第一通电话

最近更新时间: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
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

实现步骤

发起通话前,请确保完成登录,这是服务可用的必要前提。以下将分 5 步为您讲解如何"拨打一通电话"。

步骤1:创建通话界面

您需要创建一个通话页面,当发起通话时唤起通话页面,实现方式如下:
1. 创建通话页面:您可以新建一个 UIViewController 作为通话宿主页面,用于响应来电时的跳转逻辑。
2. 通话页面绑定 CallCoreView : 通话视图核心组件,自动监听 CallStore 数据并完成画面渲染,同时提供布局切换、头像与图标配置等 UI 定制化能力。
iOS
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 添加挂断按钮:创建并添加挂断按钮,在点击事件中调用 hangup 并销毁界面。
import UIKit
import AtomicXCore
import Combine

class CallViewController: UIViewController {
private lazy var buttonHangup: UIButton = {
let buttonWidth: CGFloat = 80
let buttonHeight: CGFloat = 80
let spacing: CGFloat = 30
let bottomMargin: CGFloat = 80
let totalWidth = buttonWidth * 3 + spacing * 2
let startX = (view.bounds.width - totalWidth) / 2
let buttonY = view.bounds.height - bottomMargin - buttonHeight
let button = createButton(
frame: CGRect(x: startX + (buttonWidth + spacing) * 2, y: buttonY, width: buttonWidth, height: buttonHeight),
title: "挂断"
)
button.backgroundColor = .systemRed
button.addTarget(self, action: #selector(touchHangupButton), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
// 其他初始化代码
// 1.添加挂断按钮
view.addSubview(buttonHangup)
}
@objc private func touchHangupButton() {
// 2.在点击事件中调用 hangup 接口并销毁页面
CallStore.shared.hangup(completion: nil)
}

private func createButton(frame: CGRect, title: String) -> UIButton {
let button = UIButton(type: .system)
button.frame = frame
button.setTitle(title, for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = UIColor(white: 0.3, alpha: 0.8)
button.layer.cornerRadius = frame.width / 2
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
return button
}
}
1.2 添加麦克风开关按钮:创建并添加麦克风开关按钮,并在点击事件中调用 openLocalMicrophonecloseLocalMicrophone 接口。
import UIKit
import AtomicXCore
import Combine

class CallViewController: UIViewController {
private lazy var buttonMicrophone: UIButton = {
let buttonWidth: CGFloat = 80
let buttonHeight: CGFloat = 80
let spacing: CGFloat = 30
let bottomMargin: CGFloat = 80
let totalWidth = buttonWidth * 3 + spacing * 2
let startX = (view.bounds.width - totalWidth) / 2
let buttonY = view.bounds.height - bottomMargin - buttonHeight
let button = createButton(
frame: CGRect(x: startX + buttonWidth + spacing, y: buttonY, width: buttonWidth, height: buttonHeight),
title: "麦克风"
)
button.addTarget(self, action: #selector(touchMicrophoneButton), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
// 其他初始化代码
// 1.添加麦克风开关按钮
view.addSubview(buttonMicrophone)
}
// 2.点击事件调用开启或关闭麦克风
@objc private func touchMicrophoneButton() {
let microphoneStatus = DeviceStore.shared.state.value.microphoneStatus
if microphoneStatus == .on {
DeviceStore.shared.closeLocalMicrophone()
} else {
DeviceStore.shared.openLocalMicrophone(completion: nil)
}
}
// 创建圆形按钮辅助方法
private func createButton(frame: CGRect, title: String) -> UIButton {
let button = UIButton(type: .system)
button.frame = frame
button.setTitle(title, for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = UIColor(white: 0.3, alpha: 0.8)
button.layer.cornerRadius = frame.width / 2
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
return button
}
}
1.3 添加摄像头开关按钮:在底部工具栏容器中添加摄像头开关按钮,并在点击事件中调用 openLocalCameracloseLocalCamera 接口。
import UIKit
import AtomicXCore
import Combine

class CallViewController: UIViewController {
private lazy var buttonCamera: UIButton = {
let buttonWidth: CGFloat = 80
let buttonHeight: CGFloat = 80
let spacing: CGFloat = 30
let bottomMargin: CGFloat = 80
let totalWidth = buttonWidth * 3 + spacing * 2
let startX = (view.bounds.width - totalWidth) / 2
let buttonY = view.bounds.height - bottomMargin - buttonHeight
let button = createButton(
frame: CGRect(x: startX, y: buttonY, width: buttonWidth, height: buttonHeight),
title: "摄像头"
)
button.addTarget(self, action: #selector(touchCameraButton), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
// 其他初始化代码
// 1.添加摄像头开关按钮
view.addSubview(buttonCamera)
}

// 2.摄像头按钮点击事件
@objc private func touchCameraButton() {
let cameraStatus = DeviceStore.shared.state.value.cameraStatus
if cameraStatus == .on {
DeviceStore.shared.closeLocalCamera()
} else {
let isFront = DeviceStore.shared.state.value.isFrontCamera
DeviceStore.shared.openLocalCamera(isFront: isFront, completion: nil)
}
}
// 创建圆形按钮辅助方法
private func createButton(frame: CGRect, title: String) -> UIButton {
let button = UIButton(type: .system)
button.frame = frame
button.setTitle(title, for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = UIColor(white: 0.3, alpha: 0.8)
button.layer.cornerRadius = frame.width / 2
button.titleLabel?.font = UIFont.systemFont(ofSize: 14)
return button
}
}
1.4 实时更新媒体设备按钮文本:监听麦克风和摄像头的状态,实时更新按钮文本。
import UIKit
import AtomicXCore
import Combine

class CallViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// 其他初始化代码
// 1.监听麦克风和摄像头的状态
observeDeviceState()
}
private func observeDeviceState() {
DeviceStore.shared.state.subscribe()
.map { $0.cameraStatus }
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink {
// 2.更新摄像头按钮文本
[weak self] cameraStatus in
let title = cameraStatus == .on ? "关闭摄像头" : "开启摄像头"
self?.buttonCamera?.setTitle(title, for: .normal)
}
.store(in: &cancellables)
DeviceStore.shared.state.subscribe()
.map { $0.microphoneStatus }
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink {
// 2.更新麦克风按钮文本
[weak self] microphoneStatus in
let title = microphoneStatus == .on ? "关闭麦克风" : "开启麦克风"
self?.buttonMicrophone?.setTitle(title, for: .normal)
}
.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:发起通话

您可以在 calls 调用成功后跳转通话界面,我们建议您根据媒体类型自动开启麦克风或摄像头获得更好的通话体验,实现方式如下:
1. 发起通话:调用 calls 发起通话。
2. 开启媒体设备:发起通话成功后开启麦克风,如果是视频通话同时开启摄像头。
3. 唤起通话页面:发起通话成功,唤起通话页面。
import UIKit
import AtomicXCore
import Combine

class MainViewController: UIViewController {
// 1.发起通话
private func startCall(userIdList: [String], mediaType: CallMediaType) {
var params = CallParams()
params.timeout = 30 // 设置通话超时时间为30秒

CallStore.shared.calls(
participantIds: userIdList,
callMediaType: mediaType, // 通话类型:.audio(音频) 或 .video(视频)
params: params
) { [weak self] result in
switch result {
case .success:
// 2.开启媒体设备
self?.openDevices(for: mediaType)
// 3.唤起通话页面
DispatchQueue.main.async {
let callVC = CallViewController()
callVC.modalPresentationStyle = .fullScreen
self?.present(callVC, animated: true)
}
case .failure(let error):
Log.error("发起通话失败: \\(error)")
}
}
}
private func openDevices(for mediaType: CallMediaType) {
DeviceStore.shared.openLocalMicrophone(completion: nil)
if mediaType == .video {
let isFront = DeviceStore.shared.state.value.isFrontCamera
DeviceStore.shared.openLocalCamera(isFront: isFront, completion: nil)
}
}
}

calls 接口参数详细说明:
参数
类型
是否必填
说明
participantIds
List<String>
目标用户的 userId 列表。
callMediaType
通话媒体类型,用于指定发起音频通话还是视频通话。
CallMediaType.video : 视频通话。
CallMediaType.audio : 语音通话。
params
通话扩展参数,如:房间号、通话邀请超时时间等。
roomId (String) : 房间 ID,可选参数,未指定时由服务端自动分配。
timeout (Int) : 呼叫超时时间(秒)。
userData (String) : 用户自定义数据。
chatGroupId (String) : Chat 群组 ID,用于群组通话场景。
isEphemeralCall (Boolean) : 是否为加密通话(不产生通话记录)。

步骤5:结束通话

无论您调用 hangup 挂断还是对方主动结束通话,均会触发 onCallEnded 事件。 建议监听该事件,当该事件被触发(即通话结束)时,执行关闭当前界面的操作。实现方式如下:
1. 监听通话结束事件:监听 onCallEnded 事件。
2. 销毁通话页面onCallEnded 触发后,销毁通话页面。
import UIKit
import AtomicXCore
import Combine

class CallViewController: UIViewController {
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)
}
}
onCallEnded 事件参数详细说明
参数
类型
说明
callId
String
此次通话的唯一标识。
mediaType
通话媒体类型,用于指定发起音频通话还是视频通话。
CallMediaType.video : 视频通话。
CallMediaType.audio : 语音通话。
reason
通话结束的原因。
unknown : 未知原因,无法确定结束原因。
hangup : 正常挂断,用户主动挂断通话。
reject : 拒绝接听,被叫方拒绝来电。
noResponse : 无响应,被叫方未在超时时间内接听。
offline : 对方离线,被叫方不在线。
lineBusy : 对方忙线,被叫方正在通话中。
canceled : 通话取消,主叫方在对方接听前取消。
otherDeviceAccepted : 其他设备已接听,通话已在另一登录设备上接听。
otherDeviceReject : 其他设备已拒绝,通话已在另一登录设备上拒绝。
endByServer : 服务器结束,通话被服务器终止。
userId
String
触发结束的用户 ID。

运行效果

当您完成以上 5 步后,"拨打一通电话"运行效果如下:


定制页面

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

自定义音量提示的图标

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

setVolumeLevelIcons 示例代码:
iOS
// 设置音量提示图标
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 示例代码:
iOS
// 设置网络质量图标
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 示例代码:
iOS
// 设置用户头像
var avatars: [String: String] = [:]
let userId = "" // 用户 ID
let avatarPath = "" // 用户默认头像资源的路径
avatars[userId] = avatarPath

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

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

自定义 loading 动画

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

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 {
// 1.数据层订阅
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 示例代码
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

切换布局模式

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



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

设置通话的默认超时时间

您可以在发起通话 calls 时,通过配置参数 CallParams 中的 timeout 字段来指定等待超时时间。示例代码如下:
var callParams = CallParams()
callParams.timeout = 30 // 设置通话超时为30秒
CallStore.shared.calls(
participantIds: userIdList,
callMediaType: .video,
params: callParams,
completion: nil
)
参数
类型
是否必填
说明
userIdList
List<String>
目标用户的 userId 列表。
mediaType
通话媒体类型,用于指定发起音频通话还是视频通话。
CallMediaType.video : 视频通话。
CallMediaType.audio : 语音通话。
params
通话扩展参数,如:房间号、通话邀请超时时间等。
roomId (String) : 房间 ID,可选参数,未指定时由服务端自动分配。
timeout (Int) : 呼叫超时时间(秒)。
userData (String) : 用户自定义数据。
chatGroupId (String) : Chat 群组 ID,用于群组通话场景。
isEphemeralCall (Boolean) : 是否为加密通话(不产生通话记录)。

实现应用内悬浮窗

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 Foundation
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
}
}

播放等待接听的提示音

您可以监听自己的通话状态,在等待接听时播放铃声,在接听通话或通话结束时停止播放铃声。
import Combine

private var cancellables = Set<AnyCancellable>()

private func observeSelfCallStatus() {
CallStore.shared.state.subscribe()
.map { $0.selfInfo.status }
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] status in
if status == .accept || status == .none {
// 停止播放铃声
return
}
if status == .waiting {
// 播放铃声
}
}
.store(in: &cancellables)
}

开启后台采集音频/视频

为了确保应用在进入后台时仍能正常采集音频和视频(例如用户锁屏或切换到其他应用),您需要配置 iOS 的后台模式权限并设置音频会话。
配置步骤:在 Xcode 中打开您的项目,按照以下步骤操作:
1. 选择项目的 TargetSigning & Capabilities
2. 点击 + Capability
3. 搜索并添加 Background Modes
4. 勾选以下三个选项:
Audio, AirPlay, and Picture in Picture(保持音频采集和画中画功能)。
Voice over IP(支持 VoIP 通话)。
Remote notifications(可选,用于接收离线推送)。
配置完成后,您的 Info.plist 文件会自动添加以下内容:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>voip</string>
<string>remote-notification</string>
</array>
配置音频会话(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 {
// 音频会话配置失败
}
}
播放铃声时的特殊处理(可选)
如果您需要在等待接听时播放铃声,并且希望铃声通过扬声器播放,可以临时切换音频会话为 .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 {
// 恢复音频会话失败
}
}

设置离线推送参数

您可参照 自集成推送 接入该功能。接入后,离线来电默认提示语为 "You have a new call"。若您有自定义推送内容的需求,欢迎随时联系我们以反馈您的诉求。

下一步

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

常见问题

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

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

联系我们

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