Skip to main content
Build a custom participant list UI that displays real-time participant information with full control over layout and interactions. This guide demonstrates how to hide the default participant list and create your own using participant events and actions.

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] = []
    
    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:
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) {
        self.participant = participant
        nameLabel.text = participant.name
        
        // Build status text
        var statusParts: [String] = []
        if participant.isAudioMuted { statusParts.append("🔇 Muted") }
        if participant.isVideoPaused { statusParts.append("📹 Video Off") }
        if participant.isPresenting { statusParts.append("🖥️ Presenting") }
        if participant.raisedHandTimestamp > 0 { statusParts.append("✋ Hand Raised") }
        if participant.isPinned { statusParts.append("📌 Pinned") }
        
        statusLabel.text = statusParts.isEmpty ? "Active" : statusParts.joined(separator: " • ")
        
        // Update button states
        muteButton.alpha = participant.isAudioMuted ? 0.5 : 1.0
        pinButton.tintColor = participant.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) {
        print("\(participant.name) left")
    }
    
    func onParticipantAudioMuted(participant: Participant) {
        // Table will update via onParticipantListChanged
    }
    
    func onParticipantAudioUnmuted(participant: Participant) {}
    func onParticipantVideoPaused(participant: Participant) {}
    func onParticipantVideoResumed(participant: Participant) {}
    func onParticipantHandRaised(participant: Participant) {}
    func onParticipantHandLowered(participant: Participant) {}
}

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]
        
        cell.configure(with: participant)
        
        cell.onMuteAction = { [weak self] participant in
            CallSession.shared.muteParticipant(participant.uid)
        }
        
        cell.onPinAction = { [weak self] participant in
            if participant.isPinned {
                CallSession.shared.unPinParticipant()
            } else {
                CallSession.shared.pinParticipant(participant.uid)
            }
        }
        
        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)
    }
}