观众核心页面(iOS)

最近更新时间:2026-04-22 14:57:12

我的收藏
AudienceView 观众端核心 UI 组件。通过该组件,开发者可以快速搭建基础的观众直播界面。本文档将按照从基础按钮调整到复杂视图替换的顺序,指导开发者完成观众端界面的按需定制。
页面结构示意图
页面结构示意图


准备工作

在开始调整观众端界面前,请先参考 观众观看 完成观众进房的主流程搭建。

功能概览

与主播端直接创建并使用视图的结构不同,观众端由于需要支持上下滑动无缝切换直播间,AudienceView 是一个滑动容器。
在观众端,界面定制不是直接在 AudienceView 上进行的,而是作用于它内部的单房间视图 AudienceLiveView。滑动容器在工作时会动态创建和展示这些单房间视图,开发者需要通过 AudienceViewDelegate 协议,在以下核心生命周期回调中获取到对应房间的视图实例(即回调参数中的 liveView),并在正确的时机对其进行定制与资源管理:
方法
描述
audienceView(_:onCreateLiveView liveView:for:)
视图创建回调。此回调会向外传递刚刚实例化的 AudienceLiveView
适用于提前确定的静态样式定制。由于容器会提前预加载下个房间,该回调触发时视图可能尚未显示在屏幕上。通常在这里通过 liveView 执行替换内置组件、配置固定按钮等一次性布局操作。
audienceView(_:liveViewDidAppear liveView:for:)
视图展示回调。此回调会向外传递当前真正显示在屏幕上的 AudienceLiveView
适用于依赖实时状态或信令的动态调整。通常在这里记录当前活跃的 liveView 实例,以便在收到业务信令(如商品上架通知)时,精准定向更新当前屏幕上的 UI;或在此处开启房间专属的定时器与动效。
audienceView(_:liveViewDidDisappear liveView:for:)
视图隐藏回调。当观众滑出该房间或关闭界面时触发。
适用于状态重置与资源清理。通常在这里清空活跃的 liveView 记录,销毁在该房间内弹出的自定义业务面板、停止动效或清理定时器。
单房间视图 AudienceLiveView 提供的核心自定义接口与属性如下:
方法/属性
描述
topRightItems
用于灵活配置直播间右上角的按钮集合,支持自由添加自定义按钮或调整内置按钮的布局。
bottomItems
用于灵活配置直播间底部的按钮集合,支持自由添加自定义按钮或调整内置按钮的布局。
replace(node:with:)
用于将指定位置的默认组件(如顶部信息区、底部操作栏),替换为开发者自定义的全新视图。
overlayView
专属挂件图层,方便开发者自由添加需要在视频画面上方悬浮展示的全局业务 UI。
perform(action:)
用于在自定义视图中直接触发内置默认逻辑,例如显示默认观众列表,显示默认连麦管理面板等。

快速开始

下面示例快速搭建一个带货直播间观看页。主要流程包括:
隐藏不需要的连麦按钮并添加购物车按钮。
模拟接收商品上架通知并弹出商品卡片。
最后将点击事件路由至商品列表页。
import UIKit
import TUILiveKit
import SnapKit

class AudienceViewController: UIViewController {
weak var currentLiveView: AudienceLiveView?
override func viewDidLoad() {
super.viewDidLoad()
// 步骤 1:初始化 AudienceView 并添加到当前控制器
let audienceView = AudienceView(roomId: "your_room_id")
audienceView.delegate = self
view.addSubview(audienceView)
audienceView.frame = view.bounds
}
// 业务方法:展示商品列表页
func showProductListPanel() {
print("展示商品列表页...")
}
// 业务方法:模拟收到 IM 商品推送通知
func onReceiveProductPushMessage() {
// 步骤 5:通过信令动态调整 UI 时,必须作用于当前正在展示的 liveView
guard let liveView = currentLiveView else { return }
let productCard = AudienceProductCardView() // 自定义商品卡片视图
let productTap = UITapGestureRecognizer(target: self, action: #selector(onProductCardTapped))
productCard.addGestureRecognizer(productTap)
liveView.overlayView.addSubview(productCard)
productCard.snp.makeConstraints { make in
make.trailing.equalToSuperview().offset(-12)
make.bottom.equalToSuperview().offset(-100)
make.width.equalTo(150)
}
}
}

extension AudienceViewController: AudienceViewDelegate {
// 步骤 2:在创建回调中,进行静态的 UI 样式配置
func audienceView(_ audienceView: AudienceView, onCreateLiveView liveView: AudienceLiveView, for liveInfo: LiveInfo) {
// 配置底部操作栏:隐藏连麦功能,并添加自定义“购物车”按钮
let shopCartBtn = UIButton(type: .custom)
shopCartBtn.setImage(UIImage(named: "shop_cart_icon"), for: .normal)
shopCartBtn.addTarget(self, action: #selector(onShopCartTapped), for: .touchUpInside)
// 重新排列底部栏:仅保留礼物、点赞,插入购物车按钮(不声明 .coGuest 即可隐藏连麦)
liveView.bottomItems = [.gift, .like, .custom(shopCartBtn)]
}
// 步骤 3:在展示回调中,记录当前活跃视图,为后续动态信令更新做准备
func audienceView(_ audienceView: AudienceView, liveViewDidAppear liveView: AudienceLiveView, for liveInfo: LiveInfo) {
self.currentLiveView = liveView
}
// 步骤 4:在隐藏回调中,清理业务状态或销毁弹窗,防止视图复用导致状态错乱
func audienceView(_ audienceView: AudienceView, liveViewDidDisappear liveView: AudienceLiveView, for liveInfo: LiveInfo) {
if self.currentLiveView === liveView {
self.currentLiveView = nil
}
}
// 步骤 6:处理业务点击事件
@objc func onShopCartTapped() { showProductListPanel() }
@objc func onProductCardTapped() { showProductListPanel() }
}

class AudienceProductCardView: UIView {
// 自定义的商品卡片
}

调整底部操作按钮

底部工具栏是观众进行互动的核心区域。目前默认提供礼物、连麦、点赞、更多等按钮。开发者可通过 bottomItems 属性灵活增删内置功能,或通过 .custom(UIView) 插入自定义按钮。


实现步骤

步骤 1:准备自定义按钮视图。按需创建自定义按钮视图对象。
步骤 2:更新底部按钮数组。实现 AudienceViewDelegate 回调,在相应的回调方法中把组装好的包含 AudienceBottomItem 枚举的数组重新赋值给视图属性。
func audienceView(_ audienceView: AudienceView,
onCreateLiveView liveView: AudienceLiveView,
for liveInfo: LiveInfo) {
// 步骤 1:准备自定义按钮视图
let shopButton = UIButton(type: .custom)
shopButton.setImage(UIImage(named: "shop_cart"), for: .normal)
// 步骤 2:更新底部按钮数组,保留送礼,隐藏连麦,新增商品按钮
liveView.bottomItems = [
.gift,
.custom(shopButton)
]
}

调整顶部操作按钮

顶部区域展示房间信息与关键操作。默认提供观众人数、悬浮窗、退出三个按钮。开发者可通过 topRightItems 属性精简或增加控制按钮。


实现步骤

步骤 1:准备自定义按钮视图。按需创建自定义按钮视图对象。
步骤 2:更新顶部按钮数组。实现 AudienceViewDelegate 回调,在相应的回调方法中将枚举项或自定义视图赋值给 topRightItems 属性。
func audienceView(_ audienceView: AudienceView,
onCreateLiveView liveView: AudienceLiveView,
for liveInfo: LiveInfo) {
// 步骤 1:准备自定义按钮视图
let reportButton = UIButton(type: .custom)
reportButton.setImage(UIImage(named: "report_btn"), for: .normal)

// 步骤 2: 更新顶部按钮数组(保留人数和关闭,新增举报)
liveView.topRightItems = [.audienceCount, .custom(reportButton), .close]
}

替换界面指定区域视图

当调整按钮无法满足结构性修改需求时,可使用 replace 接口整体替换指定区域的视图。内部通过 AudienceNode 枚举定义了 5 个支持替换的区域,可以结合开头的“页面结构示意图”理解:
AudienceNode
说明
liveInfo
左上角的主播与房间信息展示区域。
topRightButtons
右上角的系统控制按钮区域。
networkInfo
网络状态指示区域。
bottomRightBar
右下角的业务操作栏区域。
barrageInput
左下角的弹幕输入框触发区域。

布局规则

replace 接口会将自定义视图放入指定的 slot 区域。视图的位置由框架控制,开发者无需设置。视图的尺寸由其自身决定,推荐使用以下两种方式之一声明尺寸:

方式 1:使用内部约束链(推荐)

确保子视图的约束形成完整链条,从而自动撑开父视图。
class MyInfoView: UIView {
init() {
super.init(frame: .zero)
let label = UILabel()
label.text = "直播中"
addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(12) //由子视图撑开视图
}
}
}

方式 2:重写 intrinsicContentSize

通过重写属性直接指定视图的固定尺寸。
class MyInfoView: UIView {
override var intrinsicContentSize: CGSize {
CGSize(width: 200, height: 44)
}
}

实现步骤

步骤 1:创建自定义视图对象,确保符合上文所述的布局规则。
步骤 2:实现 AudienceViewDelegate 回调,在相应的回调方法中调用 AudienceLiveView 组件的 replace 接口,传入需要替换的节点枚举和新建的自定义视图。
func audienceView(_ audienceView: AudienceView,
onCreateLiveView liveView: AudienceLiveView,
for liveInfo: LiveInfo) {
// 步骤 1:初始化符合布局规则的自定义视图
let customInfoView = MyInfoView()
customInfoView.backgroundColor = .darkGray
// 步骤 2:调用替换接口更新特定节点
liveView.replace(node: .liveInfo, with: customInfoView)
}

绑定事件与触发逻辑

替换节点后,开发者需自行接管该视图的交互事件。可在事件回调中执行专属业务逻辑,也可以使用 perform(action:) 方法快速触发内置逻辑。支持的 AudienceAction 包括:展示礼物面板(.showGiftPanel)、展示观众列表(.showAudienceList)等。

实现步骤

步骤 1:为自定义视图绑定事件,使用 addTarget 或手势识别器为视图添加点击事件。
步骤 2:触发内置逻辑或执行业务代码,在事件回调中调用 perform 方法传入 AudienceAction 枚举,或执行其他业务代码。
func audienceView(_ audienceView: AudienceView,
onCreateLiveView liveView: AudienceLiveView,
for liveInfo: LiveInfo) {
let btn = MyGiftButton()
btn.liveView = liveView
liveView.bottomItems = [.custom(btn)]
}

class MyGiftButton: UIButton {
weak var liveView: AudienceLiveView? // 弱引用
override init(frame: CGRect) {
super.init(frame: frame)
setImage(UIImage(named: "custom_gift"), for: .normal)
addTarget(self, action: #selector(onTap), for: .touchUpInside)
}
required init?(coder: NSCoder) { fatalError() }
@objc func onTap() {
// 弹出默认礼物面板
liveView?.perform(.showGiftPanel)
// 或者触发自定义逻辑
// presentCustomPanel()
}
}

深度定制业务弹窗

当使用 perform 触发的内置默认面板无法满足具体的业务需求时,开发者可以彻底接管这部分逻辑,基于底层数据 Core SDK 构建全新的业务面板。

核心思路

开发者可使用底层数据接口 AtomicXCore 获取房间、用户及状态数据,完全自主地完成自定义视图的搭建与交互绑定。在完成自定义控制器的构建后,建议使用内部封装的 AtomicPopover 组件将其弹出,以确保您的自定义面板也能获得与 SDK 内置面板一致的丝滑手势拦截与平滑动画。

实现步骤

步骤 1:构建自定义业务视图。创建一个独立的 UIView,并在其内部引入 AtomicXCore 接口,用于拉取或监听底层业务数据以驱动 UI 更新。
步骤 2:配置弹窗容器参数。实例化弹窗配置对象,并指定弹出位置、占用高度与动画类型。
步骤 3:弹出自定义视图。实例化您的自定义视图,将其作为 contentView 传入 AtomicPopover 容器,并通过系统方法进行展示。
import AtomicXCore // 引入底层数据接口进行业务开发
// 步骤1:构建自定义观众列表视图
class CustomAudienceListView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .white
setupUI()
bindLiveData()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
// 在此添加您的自定义 UI 控件,例如展示观众头像、用户等级等
}
private func bindLiveData() {
// 使用 AtomicXCore 提供的核心接口获取当前房间状态或用户数据
// 拿到核心数据后刷新上方构建的自定义 UI
}
}
// 示例场景:从屏幕底部向上滑出一个高度占屏幕一半的完全自定义面板
func presentBusinessPanel(from parentViewController: UIViewController) {
// 步骤2:配置弹窗容器参数
let config = AtomicPopover.AtomicPopoverConfig(
position: .bottom,
height: .ratio(0.5),
animation: .slideFromBottom
)
// 步骤3:弹出自定义视图
let audienceListView = CustomAudienceListView()
let popover = AtomicPopover(contentView: audienceListView, configuration: config)
parentViewController.present(popover, animated: true)
}
请参考以下文档,使用 AtomicXCore 接口实现自定义功能面板页
功能描述
参考文档
实现观众连线管理面板:连麦申请 / 邀请 / 同意 / 拒绝,连麦成员权限控制(麦克风 / 摄像头),状态同步。
实现主播跨房连线面板:连线主播互动管理,发起 / 接受 / 拒绝连线。
实现观众列表:统计观众数量,监听观众进出事件。
实现音频特效面板:变声(童声 / 男声)、混响(KTV 等)、耳返调节,实时切换特效。
音效

添加自定义悬浮挂件

复杂的直播场景常需要在视频画面上方悬浮展示活动图标或互动贴纸。这类需要定位于视频层之上、且独立于基础布局的视图,应统一添加到 overlayView 图层中。
在实际业务中,悬浮挂件通常作为某个活动面板的入口。建议将其与前文介绍的 AtomicPopover 组件结合使用:为挂件绑定点击事件,并在触发后弹出深度定制的业务弹窗。

挂件生命周期管理

结合视图的生命周期回调,悬浮挂件的添加与管理时机取决于具体的业务需求:
常驻型挂件(例如直播间固定的活动入口):可以直接在 onCreateLiveView 回调中将其添加至 overlayView
动态型挂件(例如天降红包、临时弹出商品卡片):应在接收到业务信令后,获取 liveViewDidAppear 记录的当前活跃视图实例,再向其 overlayView 中动态添加控件。
挂件与弹窗销毁:对于动态弹出的业务面板或挂件,可以在 liveViewDidDisappear 回调中执行视图的 removeFromSuperview 操作。这能有效避免用户滑动切房时,因视图复用导致的 UI 状态异常。

实现步骤

步骤 1:创建挂件视图并开启交互。实例化悬浮控件,设置其尺寸与位置。
步骤 2:绑定点击事件。为挂件添加手势识别器,用于响应观众的点击操作。
步骤 3:添加至覆盖图层并联动弹窗。将挂件添加至 overlayView 中,并在点击回调中调用前文定义的自定义弹窗逻辑。
import UIKit
import TUILiveKit
class LiveRoomController: UIViewController {
private weak var currentLiveView: AudienceLiveView? // 在AudienceViewDelegate中获取当前的liveView
// 示例场景:在画面左上角悬浮显示红包挂件,点击后联动弹出前文定义的业务面板
func addRedPacketWidget() {
// 步骤 1:创建挂件视图并开启交互
let redPacketWidget = UIImageView(image: UIImage(named: "red_packet_icon"))
redPacketWidget.frame = CGRect(x: 15, y: 120, width: 60, height: 60)
redPacketWidget.isUserInteractionEnabled = true
// 步骤 2:绑定点击事件
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleRedPacketClick))
redPacketWidget.addGestureRecognizer(tapGesture)
// 步骤 3:添加至覆盖图层
currentLiveView?.overlayView.addSubview(redPacketWidget)
}
@objc func handleRedPacketClick() {
// 在此处调用前文写好的 presentBusinessPanel 方法,弹出深度定制的业务视图
// presentBusinessPanel(from: self)
}
}

常见问题

添加的自定义按钮为何点击无响应?

使用.custom()传入自定义视图后,点击视图却无法触发绑定的事件。
排查建议:
检查视图尺寸与内部约束(高频原因):这是最容易被忽略的问题。如果您传入了一个自定义的容器视图(例如替换了整个 bottomRightBar),请务必确保内部子控件形成了完整的约束链以撑开父视图,或者为主视图重写了 intrinsicContentSize。如果父视图实际尺寸为 0,即使内部按钮在屏幕上可见,点击事件也会因超出父视图边界而被系统直接丢弃。
检查交互属性:检查 isUserInteractionEnabled 属性。如果是 UIImageView 或普通的 UIView 容器,系统默认该属性为 false,必须手动将其设置为 true
检查手势作用域:如果自定义视图(或其子控件)的布局超出了其直接父容器的边界,超出部分的点击事件将无法响应。
检查事件绑定与目标(Target):确认是否正确添加了 UITapGestureRecognizer 或调用了 addTarget 方法。另外,由于视图是在代理回调中动态生成的,请确保事件绑定的目标对象(如外部的 controller)在视图存活期间未被释放。

动态隐藏或显示操作按钮?

在实际业务中,可能需要根据房间状态(例如带货期间隐藏连麦按钮),动态调整底部或顶部的工具栏。
实现方案 AudienceLiveViewbottomItemstopRightItems 属性支持响应式更新。只需组装一个新的按钮数组并重新赋值,SDK 内部会自动触发视图刷新,无需手动重绘。
排查建议:
在接收到信令并动态更新视图时,务必确保更新的是当前正在展示的视图实例。请使用 liveViewDidAppear 回调中记录的活跃实例,切勿误修改处于“预加载”状态的相邻房间视图。

替换视图后出现尺寸异常?

替换指定区域的视图后,如果发现 UI 压缩或超出屏幕,通常是由于约束不完整导致的。
排查建议
请确保主视图内部的子控件形成了完整的约束链,或者为主视图重写了 intrinsicContentSize 属性。在滑动容器中,视图尺寸的自适应尤为重要,约束不完整极易导致视图在滑动切换时发生重叠或变形。

使用 replace 接口替换的视图在滑动时不显示?

在使用 replace(node:with:) 接口替换指定节点后,发现当开始滑动后当前屏幕上替换的自定义视图消失了,并且之后在滑动过程中一直不显示。这通常是因为开发者在不同的直播间视图中,错误地复用了同一个自定义视图实例。
原因剖析:
由于 AudienceView 是一个滑动容器,为了保证滑动的流畅性,系统会提前预加载接下来的相邻房间。这意味着 onCreateLiveView 代理方法会被提前调用。在 iOS 中,一个 UIView 实例同一时刻只能有一个父视图。如果您传入了一个共享的全局视图实例,当预加载下一个房间时,该视图就会被系统从当前可视房间中强制移除,并添加到尚未展示的预加载房间中,从而导致当前屏幕上看不到该视图。
错误示例(请避免这样使用)
// ❌ 错误做法:在外部持有一个共享的视图实例
let sharedBrandView = MyBrandView()

public func audienceView(_ audienceView: AudienceView, onCreateLiveView liveView: AudienceLiveView, for liveInfo: LiveInfo) {
// ⚠️ 警告:当预加载下一个 liveView 时,sharedBrandView 会被从当前屏幕拔出,塞进下一个不可见的 liveView 中
liveView.replace(node: .liveInfo, with: sharedBrandView)
}
正确做法
请确保在 onCreateLiveView 回调中,每次都为新的 AudienceLiveView 创建一个全新的自定义视图实例。
public func audienceView(_ audienceView: AudienceView, onCreateLiveView liveView: AudienceLiveView, for liveInfo: LiveInfo) {
// ✅ 正确做法:每次触发回调时,都实例化一个全新的自定义视图
let newBrandView = MyBrandView()
liveView.replace(node: .liveInfo, with: newBrandView)
}