Skip to main content
Build a user profile screen with avatar display, messaging, audio/video calls, and block/unblock functionality using CometChat UIKit.

Prerequisites

Before implementing this feature, ensure you have:
  • Completed Getting Started setup
  • CometChat UIKit v5+ installed
  • User logged in with CometChatUIKit.login()
  • Navigation controller configured

Overview

The user details screen provides:
  • Profile Display - Avatar, name, and online status
  • Messaging - Start one-on-one chats
  • Calling - Audio and video call buttons
  • Block/Unblock - Manage user relationships
  • Delete Conversation - Remove chat history

Basic Implementation

Create a simple user details screen:
import UIKit
import CometChatUIKitSwift
import CometChatSDK

class UserDetailsViewController: UIViewController {
    
    private var user: User?
    
    convenience init(user: User) {
        self.init()
        self.user = user
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        setupUI()
    }
    
    private func setupUI() {
        guard let user = user else { return }
        
        // Avatar
        let avatar = CometChatAvatar()
        avatar.set(user: user)
        avatar.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(avatar)

        // Name label
        let nameLabel = UILabel()
        nameLabel.text = user.name
        nameLabel.font = .systemFont(ofSize: 24, weight: .bold)
        nameLabel.textAlignment = .center
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(nameLabel)
        
        // Status label
        let statusLabel = UILabel()
        statusLabel.text = user.status == .online ? "Online" : "Offline"
        statusLabel.textColor = user.status == .online ? .systemGreen : .secondaryLabel
        statusLabel.font = .systemFont(ofSize: 14)
        statusLabel.textAlignment = .center
        statusLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(statusLabel)
        
        // Message button
        let messageButton = UIButton(type: .system)
        messageButton.setTitle("Message", for: .normal)
        messageButton.setImage(UIImage(systemName: "message.fill"), for: .normal)
        messageButton.addTarget(self, action: #selector(openChat), for: .touchUpInside)
        messageButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(messageButton)
        
        // Block button
        let blockButton = UIButton(type: .system)
        blockButton.setTitle("Block User", for: .normal)
        blockButton.setTitleColor(.systemRed, for: .normal)
        blockButton.addTarget(self, action: #selector(blockUser), for: .touchUpInside)
        blockButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(blockButton)
        
        NSLayoutConstraint.activate([
            avatar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40),
            avatar.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            avatar.widthAnchor.constraint(equalToConstant: 100),
            avatar.heightAnchor.constraint(equalToConstant: 100),
            
            nameLabel.topAnchor.constraint(equalTo: avatar.bottomAnchor, constant: 16),
            nameLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            
            statusLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
            statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            
            messageButton.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 32),
            messageButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            
            blockButton.topAnchor.constraint(equalTo: messageButton.bottomAnchor, constant: 24),
            blockButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    }
    
    @objc private func openChat() {
        guard let user = user else { return }
        let messages = CometChatMessages()
        messages.set(user: user)
        navigationController?.pushViewController(messages, animated: true)
    }
    
    @objc private func blockUser() {
        guard let user = user else { return }
        CometChat.blockUsers([user.uid ?? ""]) { [weak self] _ in
            DispatchQueue.main.async {
                self?.showAlert(title: "Blocked", message: "\(user.name ?? "User") has been blocked")
            }
        } onError: { error in
            print("Block error: \(error?.errorDescription ?? "")")
        }
    }
    
    private func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

Production Implementation

Complete user details screen with all features and event handling:
import UIKit
import CometChatUIKitSwift
import CometChatSDK
#if canImport(CometChatCallsSDK)
import CometChatCallsSDK
#endif

class ProductionUserDetailsViewController: UIViewController {
    
    // MARK: - Properties
    private var user: User?
    private var isBlocked: Bool = false
    private let listenerID = "user-details-listener"
    
    // MARK: - UI Components
    private let scrollView = UIScrollView()
    private let contentView = UIView()
    private let avatar = CometChatAvatar()
    private let nameLabel = UILabel()
    private let statusLabel = UILabel()
    private let buttonStackView = UIStackView()
    private let messageButton = UIButton(type: .system)
    private let audioCallButton = UIButton(type: .system)
    private let videoCallButton = UIButton(type: .system)
    private let blockButton = UIButton(type: .system)
    private let deleteButton = UIButton(type: .system)
    
    // MARK: - Initialization
    convenience init(user: User) {
        self.init()
        self.user = user
    }
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        setupUI()
        configureWithUser()
        checkBlockStatus()
        addEventListeners()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        setupNavigationBar()
    }
    
    deinit {
        removeEventListeners()
    }

    // MARK: - Setup
    private func setupNavigationBar() {
        title = "User Details"
        navigationController?.navigationBar.prefersLargeTitles = false
    }
    
    private func setupUI() {
        // Scroll View
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(scrollView)
        
        contentView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(contentView)
        
        // Avatar
        avatar.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(avatar)
        
        // Name Label
        nameLabel.font = .systemFont(ofSize: 24, weight: .bold)
        nameLabel.textAlignment = .center
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(nameLabel)
        
        // Status Label
        statusLabel.font = .systemFont(ofSize: 14)
        statusLabel.textAlignment = .center
        statusLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(statusLabel)
        
        // Button Stack
        buttonStackView.axis = .horizontal
        buttonStackView.spacing = 24
        buttonStackView.distribution = .equalSpacing
        buttonStackView.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(buttonStackView)
        
        // Message Button
        configureButton(messageButton, title: "Message", icon: "message.fill", action: #selector(openChat))
        buttonStackView.addArrangedSubview(messageButton)
        
        // Call Buttons (conditional)
        #if canImport(CometChatCallsSDK)
        configureButton(audioCallButton, title: "Audio", icon: "phone.fill", action: #selector(startAudioCall))
        configureButton(videoCallButton, title: "Video", icon: "video.fill", action: #selector(startVideoCall))
        buttonStackView.addArrangedSubview(audioCallButton)
        buttonStackView.addArrangedSubview(videoCallButton)
        #endif
        
        // Block Button
        blockButton.setTitle("Block User", for: .normal)
        blockButton.setTitleColor(.systemRed, for: .normal)
        blockButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
        blockButton.addTarget(self, action: #selector(toggleBlock), for: .touchUpInside)
        blockButton.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(blockButton)
        
        // Delete Button
        deleteButton.setTitle("Delete Conversation", for: .normal)
        deleteButton.setTitleColor(.systemRed, for: .normal)
        deleteButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
        deleteButton.addTarget(self, action: #selector(deleteConversation), for: .touchUpInside)
        deleteButton.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(deleteButton)
        
        setupConstraints()
    }
    
    private func configureButton(_ button: UIButton, title: String, icon: String, action: Selector) {
        button.setTitle(title, for: .normal)
        button.setImage(UIImage(systemName: icon), for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 12)
        button.tintColor = .systemBlue
        button.configuration = .plain()
        button.configuration?.imagePlacement = .top
        button.configuration?.imagePadding = 8
        button.addTarget(self, action: action, for: .touchUpInside)
    }
    
    private func setupConstraints() {
        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            
            contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
            contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
            contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
            
            avatar.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 40),
            avatar.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            avatar.widthAnchor.constraint(equalToConstant: 100),
            avatar.heightAnchor.constraint(equalToConstant: 100),
            
            nameLabel.topAnchor.constraint(equalTo: avatar.bottomAnchor, constant: 16),
            nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            
            statusLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 8),
            statusLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            
            buttonStackView.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 32),
            buttonStackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            
            blockButton.topAnchor.constraint(equalTo: buttonStackView.bottomAnchor, constant: 48),
            blockButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            
            deleteButton.topAnchor.constraint(equalTo: blockButton.bottomAnchor, constant: 16),
            deleteButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            deleteButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -40)
        ])
    }

    // MARK: - Configuration
    private func configureWithUser() {
        guard let user = user else { return }
        
        avatar.set(user: user)
        nameLabel.text = user.name
        updateStatusLabel(user: user)
    }
    
    private func updateStatusLabel(user: User) {
        if isBlocked {
            statusLabel.text = "Blocked"
            statusLabel.textColor = .systemRed
        } else {
            statusLabel.text = user.status == .online ? "Online" : "Offline"
            statusLabel.textColor = user.status == .online ? .systemGreen : .secondaryLabel
        }
    }
    
    private func checkBlockStatus() {
        guard let uid = user?.uid else { return }
        
        let request = BlockedUserRequest.BlockedUserRequestBuilder()
            .set(limit: 100)
            .build()
        
        request.fetchNext { [weak self] blockedUsers in
            let isBlocked = blockedUsers?.contains { $0.uid == uid } ?? false
            DispatchQueue.main.async {
                self?.isBlocked = isBlocked
                self?.updateBlockButton()
                self?.updateStatusLabel(user: self?.user ?? User())
            }
        } onError: { error in
            print("Error checking block status: \(error?.errorDescription ?? "")")
        }
    }
    
    private func updateBlockButton() {
        let title = isBlocked ? "Unblock User" : "Block User"
        blockButton.setTitle(title, for: .normal)
    }
    
    // MARK: - Actions
    @objc private func openChat() {
        guard let user = user else { return }
        
        if isBlocked {
            showAlert(title: "User Blocked", message: "Unblock this user to send messages")
            return
        }
        
        let messages = CometChatMessages()
        messages.set(user: user)
        navigationController?.pushViewController(messages, animated: true)
    }
    
    #if canImport(CometChatCallsSDK)
    @objc private func startAudioCall() {
        guard let user = user else { return }
        
        if isBlocked {
            showAlert(title: "User Blocked", message: "Unblock this user to make calls")
            return
        }
        
        let call = Call(receiverId: user.uid ?? "", callType: .audio, receiverType: .user)
        CometChat.initiateCall(call: call) { call in
            print("Audio call initiated: \(call?.sessionID ?? "")")
        } onError: { [weak self] error in
            self?.showAlert(title: "Call Failed", message: error?.errorDescription ?? "Unable to start call")
        }
    }
    
    @objc private func startVideoCall() {
        guard let user = user else { return }
        
        if isBlocked {
            showAlert(title: "User Blocked", message: "Unblock this user to make calls")
            return
        }
        
        let call = Call(receiverId: user.uid ?? "", callType: .video, receiverType: .user)
        CometChat.initiateCall(call: call) { call in
            print("Video call initiated: \(call?.sessionID ?? "")")
        } onError: { [weak self] error in
            self?.showAlert(title: "Call Failed", message: error?.errorDescription ?? "Unable to start call")
        }
    }
    #endif
    
    @objc private func toggleBlock() {
        if isBlocked {
            unblockUser()
        } else {
            showBlockConfirmation()
        }
    }
    
    private func showBlockConfirmation() {
        let alert = UIAlertController(
            title: "Block User",
            message: "Are you sure you want to block \(user?.name ?? "this user")? They won't be able to send you messages or call you.",
            preferredStyle: .alert
        )
        
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Block", style: .destructive) { [weak self] _ in
            self?.blockUser()
        })
        
        present(alert, animated: true)
    }
    
    private func blockUser() {
        guard let uid = user?.uid else { return }
        
        CometChat.blockUsers([uid]) { [weak self] blockedUsers in
            DispatchQueue.main.async {
                self?.isBlocked = true
                self?.updateBlockButton()
                self?.updateStatusLabel(user: self?.user ?? User())
                
                // Emit event for other components
                if let user = self?.user {
                    CometChatUserEvents.emitOnUserBlock(user: user)
                }
            }
        } onError: { [weak self] error in
            self?.showAlert(title: "Error", message: error?.errorDescription ?? "Failed to block user")
        }
    }
    
    private func unblockUser() {
        guard let uid = user?.uid else { return }
        
        CometChat.unblockUsers([uid]) { [weak self] unblockedUsers in
            DispatchQueue.main.async {
                self?.isBlocked = false
                self?.updateBlockButton()
                self?.updateStatusLabel(user: self?.user ?? User())
                
                // Emit event for other components
                if let user = self?.user {
                    CometChatUserEvents.emitOnUserUnblock(user: user)
                }
            }
        } onError: { [weak self] error in
            self?.showAlert(title: "Error", message: error?.errorDescription ?? "Failed to unblock user")
        }
    }
    
    @objc private func deleteConversation() {
        let alert = UIAlertController(
            title: "Delete Conversation",
            message: "Are you sure you want to delete this conversation? This action cannot be undone.",
            preferredStyle: .alert
        )
        
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
            self?.performDeleteConversation()
        })
        
        present(alert, animated: true)
    }
    
    private func performDeleteConversation() {
        guard let uid = user?.uid else { return }
        
        CometChat.deleteConversation(conversationWith: uid, conversationType: .user) { [weak self] _ in
            DispatchQueue.main.async {
                self?.showAlert(title: "Deleted", message: "Conversation has been deleted")
            }
        } onError: { [weak self] error in
            self?.showAlert(title: "Error", message: error?.errorDescription ?? "Failed to delete conversation")
        }
    }
    
    private func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }

    // MARK: - Event Listeners
    private func addEventListeners() {
        CometChat.addUserListener(listenerID, self)
        CometChatUserEvents.addListener(listenerID, self)
    }
    
    private func removeEventListeners() {
        CometChat.removeUserListener(listenerID)
        CometChatUserEvents.removeListener(listenerID)
    }
}

// MARK: - CometChatUserDelegate
extension ProductionUserDetailsViewController: CometChatUserDelegate {
    
    func onUserOnline(user: User) {
        guard user.uid == self.user?.uid else { return }
        DispatchQueue.main.async {
            self.user = user
            self.updateStatusLabel(user: user)
        }
    }
    
    func onUserOffline(user: User) {
        guard user.uid == self.user?.uid else { return }
        DispatchQueue.main.async {
            self.user = user
            self.updateStatusLabel(user: user)
        }
    }
}

// MARK: - CometChatUserEventListener
extension ProductionUserDetailsViewController: CometChatUserEventListener {
    
    func ccUserBlocked(user: User) {
        guard user.uid == self.user?.uid else { return }
        DispatchQueue.main.async {
            self.isBlocked = true
            self.updateBlockButton()
            self.updateStatusLabel(user: user)
        }
    }
    
    func ccUserUnblocked(user: User) {
        guard user.uid == self.user?.uid else { return }
        DispatchQueue.main.async {
            self.isBlocked = false
            self.updateBlockButton()
            self.updateStatusLabel(user: user)
        }
    }
}

Launching User Details

Open the user details screen from your app:
import UIKit
import CometChatSDK

class UsersListViewController: UIViewController {
    
    func showUserDetails(for user: User) {
        let detailsVC = ProductionUserDetailsViewController(user: user)
        navigationController?.pushViewController(detailsVC, animated: true)
    }
    
    // From a users list
    func openFromUsersList() {
        let users = CometChatUsers()
        users.set(onItemClick: { [weak self] user, _ in
            self?.showUserDetails(for: user)
        })
        navigationController?.pushViewController(users, animated: true)
    }
    
    // From a conversation
    func openFromConversation(_ conversation: Conversation) {
        if let user = conversation.conversationWith as? User {
            showUserDetails(for: user)
        }
    }
}

Block/Unblock API Reference

Block Users

// Block single user
CometChat.blockUsers(["user-uid"]) { blockedUsers in
    print("Blocked \(blockedUsers?.count ?? 0) users")
} onError: { error in
    print("Error: \(error?.errorDescription ?? "")")
}

// Block multiple users
CometChat.blockUsers(["user1", "user2", "user3"]) { blockedUsers in
    print("Blocked users: \(blockedUsers?.map { $0.uid ?? "" } ?? [])")
} onError: { error in
    print("Error: \(error?.errorDescription ?? "")")
}

Unblock Users

// Unblock single user
CometChat.unblockUsers(["user-uid"]) { unblockedUsers in
    print("Unblocked \(unblockedUsers?.count ?? 0) users")
} onError: { error in
    print("Error: \(error?.errorDescription ?? "")")
}

// Unblock multiple users
CometChat.unblockUsers(["user1", "user2"]) { unblockedUsers in
    print("Unblocked users: \(unblockedUsers?.map { $0.uid ?? "" } ?? [])")
} onError: { error in
    print("Error: \(error?.errorDescription ?? "")")
}

Fetch Blocked Users

let request = BlockedUserRequest.BlockedUserRequestBuilder()
    .set(limit: 50)
    .set(direction: .blockedByMe)  // or .hasBlockedMe, .both
    .build()

request.fetchNext { blockedUsers in
    for user in blockedUsers ?? [] {
        print("Blocked: \(user.name ?? "")")
    }
} onError: { error in
    print("Error: \(error?.errorDescription ?? "")")
}

Styling

Customize the avatar appearance:
let avatarStyle = AvatarStyle()
avatarStyle.cornerRadius = CometChatCornerStyle(cornerRadius: 50)
avatarStyle.borderWidth = 2
avatarStyle.borderColor = .systemBlue
avatarStyle.backgroundColor = .systemGray5

avatar.set(style: avatarStyle)

Components Reference

ComponentPurpose
CometChatAvatarDisplay user profile picture
CometChatMessagesOpen chat interface
CometChat.blockUsers()Block users
CometChat.unblockUsers()Unblock users
CometChat.deleteConversation()Delete chat history
CometChat.initiateCall()Start audio/video calls

Edge Cases

  • Self-blocking: Hide block controls when viewing your own profile
  • Already blocked: Check block status before showing chat/call options
  • Call SDK unavailable: Conditionally show call buttons with #if canImport(CometChatCallsSDK)
  • Missing user data: Show placeholder UI for incomplete profiles

Error Handling

ErrorSolution
Block failedShow retry option, check network
Unblock failedVerify user was previously blocked
Call initiation failedCheck permissions and call SDK setup
Delete conversation failedVerify conversation exists

Troubleshooting

IssueSolution
Status not updatingVerify event listeners are added
Block state incorrectFetch blocked users list on load
Call buttons missingImport CometChatCallsSDK
Avatar not loadingCheck user has valid avatar URL