Skip to main content

Channel List

The ChatChannelListVC is the UI component used to display a list of channels matching the given query.


The Channel List screen is backed by the low-level ChatChannelListController component which fetches channels from the API and keeps the list in sync with the remote. Read more about channel list query and how ChatChannelListController works here.

Basic Usage

The first step to show the channel list screen in your app is to create ChatChannelListVC instance:

// Create `ChatChannelController` with the given query
let controller = ChatClient.shared.channelListController(query: ..., filter: ...)

// Create a view controller backed by the low-level controller
let channelListVC = ChatChannelListVC.make(with: controller)

When the ChatChannelListVC instance is created, there are multiple ways of showing it:

  1. modally
  2. inside an existing UINavigationController
  3. as a tab inside UITabBarController
  4. inside UISplitViewController
  5. as a child controller

To present the channel list modally:

let navigationVC = UINavigationController(rootViewController: channelListVC)

present(navigationVC, animated: true)

To push the channel list to an existing navigation controller:

navigationController?.pushViewController(channelListVC, animated: true)

To show the channel list as a tab:

let navigationVC = UINavigationController(rootViewController: channelListVC)

let tabBatVC = UITabBarController()
tabBatVC.viewControllers = [..., navigationVC]

To show the channel list as a main screen in split view controller and the selected channel as a detail:

let channelListNVC = UINavigationController(rootViewController: channelListVC)

let splitVC = UISplitViewController()
splitVC.preferredDisplayMode = .oneBesideSecondary
splitVC.viewControllers = [
/*optionally provide a controller shown as a detail till user opens a channel*/

To show the channel list as a child:

class ParentVC: UIViewController {
let containerView: UIView

override func viewDidLoad() {

let navigationVC = UINavigationController(rootViewController: channelListVC)
navigationVC.view.translatesAutoresizingMaskIntoConstraints = false
navigationVC.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
navigationVC.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
navigationVC.view.topAnchor.constraint(equalTo: containerView.topAnchor),
navigationVC.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
navigationVC.didMove(toParent: self)

In all the cases but the push to the existing navigation controller, the channel list is wrapped by UINavigationController before presenting to get the channel list -> channel navigation out of the box.

UI Customization

You can customize how channels are rendered in the list by replacing the ChannelListItemView component with your own. Like every component from this library, this component will also follow the theming of your application.

Channel Header

The header of the channel can be configured the same way you would configure it on a UIViewController object.

class ViewController: ChatChannelListVC {
/// ...

override open func setUpAppearance() {
title = "Chats"

/// ...

Channel List States

You can opt to show an empty, error and loading view by setting the following flag to true in the Components configuration:

Components.default.isChatChannelListStatesEnabled = true

This feature is disabled by default, having just the standard loading indicator for the loading state. By enabling this feature, the StreamChat SDK will handle the channel list view states automatically for you.

Empty ViewError ViewLoading View

You can further customize or add your own implementation of an empty, error or loading view by subclassing ChatChannelListEmptyView, ChatChannelListErrorView or ChatChannelListLoadingView respectively.

Let's see how you can customize our error view:

class CustomChatChannelListVC: ChatChannelListVC {

override open func setUpAppearance() {

channelListErrorView.backgroundColor = .red
channelListErrorView.titleLabel.text = "Data unavailable"
channelListErrorView.titleLabel.textColor = .black
channelListErrorView.retryButton.setImage(.init(systemName: ""), for: .normal)

override func layoutSubviews() {
channelListErrorView.layer.cornerRadius = 20

Empty View

Now, let's see an example of how you can totally replace our loading view with your own implementation.

class CustomChannelListLoadingView: ChatChannelListLoadingView {

lazy var customLoadingView: CustomLoadingView = {
// You implementation...

// ...

override open func setUp() {
// Set up your custom views.
// Don't call super.setUp()

override open func setUpAppearance() {
// Set up the appearance of your custom views.
// Don't call super.setUpAppearance()

override open func setUpLayout() {
// Set up the layout of your custom views.
// Don't call super.setUpLayout()

// ...

Don't forget to set your custom implementation in the Components class:

Components.default.chatChannelListLoadingView = CustomChannelLoadingView.self

You can set your custom views for the channel list states when configuring your Components instance by replacing the following views as shown above:

// In Components.swift:

public var channelListEmptyView: ChatChannelListEmptyView.Type = ChatChannelListEmptyView.self

public var channelListErrorView: ChatChannelListErrorView.Type = ChatChannelListErrorView.self

public var chatChannelListLoadingView: ChatChannelListLoadingView.Type = ChatChannelListLoadingView.self

This component uses the ChannelListRouter navigation component, you can customize this by providing your own.

Components.default.channelListRouter = CustomChannelListRouter.self

You can find more information on how the components configuration works here.

The UI SDK provides searching components out-of-the-box for the Channel List. Currently, there is two types of searching strategies that can be enabled, searching by messages or channels.

To enable it, a ChannelListSearchStrategy should be configured in the Components configuration:

// If you want to enable the search by messages
Components.default.channelListSearchStrategy = .messages
// If you want to enable the search by channels
Components.default.channelListSearchStrategy = .channels

In order to customize the search UI component, you can provide a subclass of our default components, ChatMessageSearchVC and ChatChannelSearchVC, like so:

// If you want to enable the search by messages with a custom subclass
Components.default.channelListSearchStrategy = .messages(CustomChatMessageSearchVC.self)
// If you want to enable the search by channels with a custom subclass
Components.default.channelListSearchStrategy = .channels(CustomChatChannelSearchVC.self)

Both ChatMessageSearchVC and ChatChannelSearchVC inherit from the Channel List component, so all the customization provided in the Channel List should be reflected in the search components. They also use the same cell component found in the Channel List, so in case you want to customize the search cell component, you can do it by customizing the ChannelListItemView. The Channel List Item View contains a searchResult in the content property in case the view is being used for searching.

In case your app requires a total custom search UI component you can build your own from scratch. To do so, you need to customize the ChatChannelListVC.setUp() lifecycle method and provide a UISearchController by setting the navigationItem.searchController.

Channel List Controller

The channel list component uses the ChannelListController to fetch the list of channels matching your query and to stay up-to-date with all changes. In the example above you can see that we are passing a ChannelListQuery object to create the controller. Stream Chat APIs allow you to list channels based on your own query and sort.

Channel List Query

The ChannelListQuery is the structure used to specify the query parameters for fetching the list of channels from Stream backend. It has 4 parameters in its init:

public init(
filter: Filter<ChannelListFilterScope>,
sort: [Sorting<ChannelListSortingKey>] = [],
pageSize: Int = .channelsPageSize,
messagesLimit: Int = .messagesPageSize

Let's dive deep into each one.


Filter is the main parameter for a query. You can define different filters to fetch different sets of channels for a user. Examples of some most commonly used filters:

// Assume we've already created and configured our ChatClient

// Filter for channels where our user is a member
let filter = Filter<ChannelListFilterScope>.containMembers(userIds: [client.currentUserId!])

// Filter for channels where the name starts with "Group"
let filter = Filter<ChannelListFilterScope>.autocomplete(.name, text: "Group")

// Compound Filter for channels where team is read and our user is a member
let filter = Filter<ChannelListFilterScope>.and([.equal(.team, to: "read"),
.containMembers(userIds: [client.currentUserId!])])


Sorting parameter is used to sort the list of channels returned. By default, Channel List will be sorted by their last message date (or channel creation date, if the channel is empty). Most commonly, you don't need to specify any sorting, StreamChat SDK handles this. If you'd like, you can create a custom sorting, such as:

// Sorting for always showing most crowded channels first
let sorting: [Sorting<ChannelListSortingKey>] = [.init(key: .memberCount, isAscending: true),
.init(key: .lastMessageAt, isAscending: true)]


Page size is used to specify how many channels the initial page will show. You can specify an integer value for advanced use-cases. Most commonly, you don't need to touch this.

Message Limit

messagesLimit is used to specify how many messages the initial fetch will return.

Replacing Channel List Query

In case you want to apply filters to your Channel List you can do this by replacing the channel list query of the ChatChannelListVC with either replaceQuery() function, or replaceChannelListController() in case you need more control. Here is a basic example on how you can change the Channel List data to show hidden channels only:

class CustomChatChannelListVC: ChatChannelListVC {

lazy var hiddenChannelsButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(systemName: "archive")!, for: .normal)
return button

override open func setUp() {

hiddenChannelsButton.addTarget(self, action: #selector(didTapHiddenChannelsButton), for: .touchUpInside)

override open func setUpLayout() {


@objc private func didTapHiddenChannelsButton(_ sender: Any) {
guard let currentUserId = controller.client.currentUserId else { return }
let hiddenChannelsQuery: ChannelListQuery = .init(filter: .and([
.containMembers(userIds: [currentUserId]),
.equal(.hidden, to: true)

You can see a full working example in our Demo App here.


The replacing of the Channel List Query is only available after 4.32.0 version.

Marking all Channels as Read

When you're displaying, or loading a set of channels, you may want to mark all the channels as read. For this, ChannelListController has markAllRead function:


This function will reset the unread count for all the channels the controller paginates.



public var content: ChatChannelListController 


The ChatChannelListController instance that provides channels data.

public var controller: ChatChannelListController!


open private(set) lazy var loadingIndicator: UIActivityIndicatorView 


A router object responsible for handling navigation actions of this view controller.

open lazy var router: ChatChannelListRouter 


The UICollectionViewLayout that used by ChatChannelListCollectionView.

open private(set) lazy var collectionViewLayout: UICollectionViewLayout 


The UICollectionView instance that displays channel list.

open private(set) lazy var collectionView: UICollectionView =
UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)


The CurrentChatUserAvatarView instance used for displaying avatar of the current user.

open private(set) lazy var userAvatarView: CurrentChatUserAvatarView = components


Reuse identifier of separator

open var separatorReuseIdentifier: String 


Reuse identifier of collectionViewCell

open var collectionViewCellReuseIdentifier: String 



override open func setUp() 


open func collectionView(
_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath


override open func setUpLayout() 


override open func setUpAppearance() 


open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 


open func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell


open func collectionView(
_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath
) -> UICollectionReusableView


open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) 


@objc open func didTapOnCurrentUserAvatar(_ sender: Any) 


override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) 


open func loadMoreChannels() 


open func swipeableViewWillShowActionViews(for indexPath: IndexPath) 


open func swipeableViewActionViews(for indexPath: IndexPath) -> [UIView] 


This function is called when delete button is pressed from action items of a cell.

open func deleteButtonPressedForCell(at indexPath: IndexPath) 


  • indexPath: IndexPath of given cell to fetch the content of it.


This function is called when more button is pressed from action items of a cell.

open func moreButtonPressedForCell(at indexPath: IndexPath) 


  • indexPath: IndexPath of given cell to fetch the content of it.


open func controllerWillChangeChannels(_ controller: ChatChannelListController) 


open func controller(
_ controller: ChatChannelListController,
didChangeChannels changes: [ListChange<ChatChannel>]


open func controller(_ controller: DataController, didChangeState state: DataController.State)

Did you find this page helpful?