Overview
The SDK provides participant data through events, allowing you to build custom UIs for:- Participant roster with search and filtering
- Custom participant cards with role badges or metadata
- Moderation dashboards with quick access to controls
- Attendance tracking and engagement monitoring
Prerequisites
- CometChat Calls SDK installed and initialized
- Active call session (see Join Session)
- Basic understanding of UITableView or UICollectionView
Step 1: Hide Default Participant List
Configure session settings to hide the default participant list button:let sessionSettings = CometChatCalls.sessionSettingsBuilder
.hideParticipantListButton(true)
.build()
Step 2: Create Participant List Layout
Create a custom view controller for displaying participants:class ParticipantListViewController: UIViewController {
private let tableView = UITableView()
private let searchBar = UISearchBar()
private var participants: [Participant] = []
private var filteredParticipants: [Participant] = []
/// Per-participant state the app maintains itself, keyed by `Participant.uid`, because
/// `Participant` carries no such flags. Starts empty — anything that happened before this
/// client joined is not recoverable.
fileprivate var statuses: [String: ParticipantStatus] = [:]
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupParticipantListener()
}
private func setupUI() {
title = "Participants"
view.backgroundColor = .systemBackground
// Setup search bar
searchBar.placeholder = "Search participants..."
searchBar.delegate = self
searchBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(searchBar)
// Setup table view
tableView.delegate = self
tableView.dataSource = self
tableView.register(ParticipantCell.self, forCellReuseIdentifier: "ParticipantCell")
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
// Layout constraints
NSLayoutConstraint.activate([
searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// Add close button
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .close,
target: self,
action: #selector(dismissView)
)
}
@objc private func dismissView() {
dismiss(animated: true)
}
}
Step 3: Create Participant Cell
Build a custom table view cell to display participant information.Participant carries only identity and timing fields — uid, name, avatar, mid,
state, isJoined, joinedAt, leftAt, deviceID and the total*Minutes counters. It has
no mute / video / screen-share / hand-raise / pin flags, and the SDK exposes no getter for
them. Accumulate that state in your own app from the participant events,
as below. Because those events fire only on change, a client that joins mid-call cannot recover
state that was already in effect./// Per-participant state the app maintains itself, keyed by `Participant.uid`.
struct ParticipantStatus {
var isAudioMuted = false
var isVideoPaused = false
var isPresenting = false
var isHandRaised = false
var isPinned = false
}
class ParticipantCell: UITableViewCell {
private let avatarImageView = UIImageView()
private let nameLabel = UILabel()
private let statusLabel = UILabel()
private let muteButton = UIButton(type: .system)
private let pinButton = UIButton(type: .system)
var participant: Participant?
var onMuteAction: ((Participant) -> Void)?
var onPinAction: ((Participant) -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
// Avatar
avatarImageView.layer.cornerRadius = 20
avatarImageView.clipsToBounds = true
avatarImageView.backgroundColor = .systemGray4
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(avatarImageView)
// Name label
nameLabel.font = .systemFont(ofSize: 16, weight: .semibold)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(nameLabel)
// Status label
statusLabel.font = .systemFont(ofSize: 12)
statusLabel.textColor = .secondaryLabel
statusLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(statusLabel)
// Action buttons
muteButton.setImage(UIImage(systemName: "mic.slash"), for: .normal)
muteButton.addTarget(self, action: #selector(muteButtonTapped), for: .touchUpInside)
muteButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(muteButton)
pinButton.setImage(UIImage(systemName: "pin"), for: .normal)
pinButton.addTarget(self, action: #selector(pinButtonTapped), for: .touchUpInside)
pinButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(pinButton)
// Layout
NSLayoutConstraint.activate([
avatarImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
avatarImageView.widthAnchor.constraint(equalToConstant: 40),
avatarImageView.heightAnchor.constraint(equalToConstant: 40),
nameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 12),
nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
statusLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
statusLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4),
statusLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
pinButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
pinButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
pinButton.widthAnchor.constraint(equalToConstant: 32),
muteButton.trailingAnchor.constraint(equalTo: pinButton.leadingAnchor, constant: -8),
muteButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
muteButton.widthAnchor.constraint(equalToConstant: 32)
])
}
func configure(with participant: Participant, status: ParticipantStatus) {
self.participant = participant
nameLabel.text = participant.name
// Build status text from the app's own state, not from `participant`.
var statusParts: [String] = []
if status.isAudioMuted { statusParts.append("🔇 Muted") }
if status.isVideoPaused { statusParts.append("📹 Video Off") }
if status.isPresenting { statusParts.append("🖥️ Presenting") }
if status.isHandRaised { statusParts.append("✋ Hand Raised") }
if status.isPinned { statusParts.append("📌 Pinned") }
statusLabel.text = statusParts.isEmpty ? "Active" : statusParts.joined(separator: " • ")
// Update button states
muteButton.alpha = status.isAudioMuted ? 0.5 : 1.0
pinButton.tintColor = status.isPinned ? .systemBlue : .systemGray
}
@objc private func muteButtonTapped() {
guard let participant = participant else { return }
onMuteAction?(participant)
}
@objc private func pinButtonTapped() {
guard let participant = participant else { return }
onPinAction?(participant)
}
}
Step 4: Implement Participant Events
Listen for participant updates and handle actions:extension ParticipantListViewController: ParticipantEventListener {
private func setupParticipantListener() {
CallSession.shared.addParticipantEventListener(self)
}
deinit {
CallSession.shared.removeParticipantEventListener(self)
}
func onParticipantListChanged(participants: [Participant]) {
DispatchQueue.main.async {
self.participants = participants
self.filteredParticipants = participants
self.title = "Participants (\(participants.count))"
self.tableView.reloadData()
}
}
func onParticipantJoined(participant: Participant) {
print("\(participant.name ?? "") joined")
}
func onParticipantLeft(participant: Participant) {
guard let uid = participant.uid else { return }
DispatchQueue.main.async {
self.statuses.removeValue(forKey: uid) // don't leak state for someone who left
self.tableView.reloadData()
}
}
// `onParticipantListChanged` carries no flags, so every status below comes from these
// transition events. Each one mutates the app's own `statuses` map and redraws.
func onParticipantAudioMuted(participant: Participant) { update(participant) { $0.isAudioMuted = true } }
func onParticipantAudioUnmuted(participant: Participant) { update(participant) { $0.isAudioMuted = false } }
func onParticipantVideoPaused(participant: Participant) { update(participant) { $0.isVideoPaused = true } }
func onParticipantVideoResumed(participant: Participant) { update(participant) { $0.isVideoPaused = false } }
func onParticipantHandRaised(participant: Participant) { update(participant) { $0.isHandRaised = true } }
func onParticipantHandLowered(participant: Participant) { update(participant) { $0.isHandRaised = false } }
func onParticipantStartedScreenShare(participant: Participant) { update(participant) { $0.isPresenting = true } }
func onParticipantStoppedScreenShare(participant: Participant) { update(participant) { $0.isPresenting = false } }
private func update(_ participant: Participant, _ change: @escaping (inout ParticipantStatus) -> Void) {
guard let uid = participant.uid else { return }
DispatchQueue.main.async {
var status = self.statuses[uid] ?? ParticipantStatus()
change(&status)
self.statuses[uid] = status
self.tableView.reloadData()
}
}
}
Step 5: Implement Table View Data Source
extension ParticipantListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredParticipants.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ParticipantCell", for: indexPath) as! ParticipantCell
let participant = filteredParticipants[indexPath.row]
let status = participant.uid.flatMap { statuses[$0] } ?? ParticipantStatus()
cell.configure(with: participant, status: status)
cell.onMuteAction = { participant in
guard let uid = participant.uid else { return }
CallSession.shared.muteParticipant(participantId: uid)
}
cell.onPinAction = { [weak self] participant in
guard let self, let uid = participant.uid else { return }
if self.statuses[uid]?.isPinned == true {
CallSession.shared.unpinParticipant()
self.statuses[uid]?.isPinned = false
} else {
// Only one participant can be pinned at a time — `unpinParticipant()` takes no id.
for key in Array(self.statuses.keys) { self.statuses[key]?.isPinned = false }
CallSession.shared.pinParticipant(participantId: uid, type: "pin")
self.statuses[uid, default: ParticipantStatus()].isPinned = true
}
self.tableView.reloadData()
}
return cell
}
}
extension ParticipantListViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty {
filteredParticipants = participants
} else {
filteredParticipants = participants.filter {
($0.name ?? "").localizedCaseInsensitiveContains(searchText)
}
}
tableView.reloadData()
}
}
Step 6: Present Participant List
Show the participant list from your call view controller:class CallViewController: UIViewController {
private let participantListButton = UIButton(type: .system)
private func setupParticipantListButton() {
participantListButton.setImage(UIImage(systemName: "person.3"), for: .normal)
participantListButton.addTarget(self, action: #selector(showParticipantList), for: .touchUpInside)
// Add to your view hierarchy
}
@objc private func showParticipantList() {
let participantListVC = ParticipantListViewController()
let navController = UINavigationController(rootViewController: participantListVC)
navController.modalPresentationStyle = .pageSheet
if let sheet = navController.sheetPresentationController {
sheet.detents = [.medium(), .large()]
sheet.prefersGrabberVisible = true
}
present(navController, animated: true)
}
}
Related Documentation
- Participant Management - Participant actions and events
- Events - All available event listeners
- Actions - Available call actions