This is beta documentation for Stream Chat IOS SDK v5. For the latest stable version, see the latest version (v4) .

Channel List Views

This section shows how you can customize the channel list, by replacing components with your own implementation.

Changing the Chat Channel List Item

You can swap the channel list item that is displayed in the channel list with your own implementation. In order to do that, you should implement the makeChannelListItem in the ViewFactory protocol.

Here's an example on how to do that.

public func makeChannelListItem(
    options: ChannelListItemOptions<ChannelDestination>
) -> some View {
    let listItem = ChatChannelNavigatableListItem(
        factory: self,
        channel: options.channel,
        channelName: options.channelName,
        disabled: options.disabled,
        selectedChannel: options.selectedChannel,
        channelDestination: options.channelDestination,
        onItemTap: options.onItemTap
    )
    return ChatChannelSwipeableListItem(
        factory: self,
        channelListItem: listItem,
        swipedChannelId: options.swipedChannelId,
        channel: options.channel,
        numberOfTrailingItems: options.channel.ownCapabilities.contains(.deleteChannel) ? 2 : 1,
        trailingRightButtonTapped: options.trailingSwipeRightButtonTapped,
        trailingLeftButtonTapped: options.trailingSwipeLeftButtonTapped,
        leadingSwipeButtonTapped: options.leadingSwipeButtonTapped
    )
}

The ChannelListItemOptions provides the following properties:

  • channel: the channel being displayed in the list item.
  • channelName: the display name of the channel.
  • disabled: whether user interactions with the channel are disabled. Use this while the view is being swiped to prevent accidentally tapping into the channel.
  • selectedChannel: binding of the currently selected channel selection info (channel and optional message).
  • swipedChannelId: binding of the currently swiped channel id.
  • channelDestination: closure that creates the channel destination.
  • onItemTap: called when a channel list item is tapped.
  • trailingSwipeRightButtonTapped: called when the right button of the trailing swiped area is tapped.
  • trailingSwipeLeftButtonTapped: called when the left button of the trailing swiped area is tapped.
  • leadingSwipeButtonTapped: called when the button of the leading swiped area is tapped.

The last three callbacks have no effect if you return EmptyView for the leading and trailing swipe areas. By default, the leading area returns EmptyView. In a following section, you can see how these areas can be customized.

To integrate your customization into the SDK, you will need to create a new CustomFactory (or any name you prefer), and provide your implementation of the method there. Afterwards, you will need to inject the newly created CustomFactory into our view hierarchy.

var body: some Scene {
    WindowGroup {
        ChatChannelListView(viewFactory: CustomFactory.shared)
    }
}

Changing the Avatar View

You can change the avatar view of the channel list without customizing the whole row. Implement makeChannelAvatarView in the ViewFactory:

public func makeChannelAvatarView(
    options: ChannelAvatarViewOptions
) -> some View {
    CustomChannelAvatarView(
        channel: options.channel,
        size: options.size
    )
}

The ChannelAvatarViewOptions provides:

  • channel – the channel whose avatar will be customized.
  • size – the size of the avatar.

By implementing this method, the channel avatar is consistently displayed across the channel list, the channel header and the search results.

Changing the Loading View

While the channels are loaded, a loading view is displayed with a simple animating activity indicator. To replace it with your own view, implement the makeLoadingView of the ViewFactory protocol.

class CustomFactory: ViewFactory {

    @Injected(\.chatClient) public var chatClient

    public var styles = RegularStyles()

    private init() {}

    public static let shared = CustomFactory()

    func makeLoadingView(options: LoadingViewOptions) -> some View {
        VStack {
            Text("This is custom loading view")
            ProgressView()
        }
    }
}

Changing the No Channels Available View

When there are no channels available, the SDK displays a screen with a button to start a chat. To replace this screen, implement the makeNoChannelsView in the ViewFactory.

func makeNoChannelsView(options: NoChannelsViewOptions) -> some View {
    VStack {
        Spacer()
        Text("This is our own custom no channels view.")
        Spacer()
    }
}

We also have a more in-detail article on customization of the channel list and you can find an example of how to provide a no channels available view here.

Changing the Background of the Channel List

You can change the background of the channel list to be any SwiftUI View (Color, LinearGradient, Image, etc.). Implement makeChannelListBackground in the ViewFactory:

func makeChannelListBackground(options: ChannelListBackgroundOptions) -> some View {
    LinearGradient(
        colors: [.blue.opacity(0.1), .purple.opacity(0.1)],
        startPoint: .top,
        endPoint: .bottom
    )
    .edgesIgnoringSafeArea(.bottom)
}

Changing the Row Background

To customize the background of each channel list row (for example, highlight selection differently), implement makeChannelListItemBackground in your ViewFactory:

func makeChannelListItemBackground(
    options: ChannelListItemBackgroundOptions
) -> some View {
    RoundedRectangle(cornerRadius: 12)
        .fill(options.isSelected ? Color.blue.opacity(0.1) : Color.clear)
}

The ChannelListItemBackgroundOptions provides:

  • channel – the channel for this row.
  • isSelected – whether this row is currently selected.

Changing the Divider of the Chat Channel List

You can also customize the divider between channel list items. Implement makeChannelListDividerItem in the ViewFactory:

public func makeChannelListDividerItem(options: ChannelListDividerItemOptions) -> some View {
    Divider()
}

To remove the divider entirely, return an EmptyView.

By default the divider is also shown after the last item in the list. To hide it, set showChannelListDividerOnLastItem to false in ChannelListConfig:

let channelListConfig = ChannelListConfig(showChannelListDividerOnLastItem: false)
let utils = Utils(channelListConfig: channelListConfig)
let streamChat = StreamChat(chatClient: chatClient, utils: utils)

Muted Channel Indicator Style

When a channel is muted, the SDK shows a muted indicator on the channel list item. You can control its placement via channelItemMutedStyle in ChannelListConfig. The possible values are:

  • .bottomRightCorner — shows the mute icon at the bottom-right corner of the channel avatar (default).
  • .afterChannelName — shows the mute icon inline, immediately after the channel name.
let channelListConfig = ChannelListConfig(channelItemMutedStyle: .afterChannelName)
let utils = Utils(channelListConfig: channelListConfig)
let streamChat = StreamChat(chatClient: chatClient, utils: utils)

Changing the Top Bar

makeChannelListTopView lets you add custom content above the channel list. This slot is separate from the built-in search UI, which is now provided through the searchable style modifier.

func makeChannelListTopView(
    options: ChannelListTopViewOptions
) -> some View {
    ChannelListFiltersView()
}

To remove the top view entirely, return an EmptyView.

You can add a view at the bottom of the channel list. There are two options – a footer shown when you scroll to the end of the list, and a sticky footer that's always visible.

To add a footer at the bottom of the channel list, implement makeChannelListFooterView:

public func makeChannelListFooterView(options: ChannelListFooterViewOptions) -> some View {
    SomeFooterView()
}

To add a sticky footer always visible at the bottom, implement makeChannelListStickyFooterView:

func makeChannelListStickyFooterView(options: ChannelListStickyFooterViewOptions) -> some View {
    SomeStickyFooterView()
}

Both methods return an EmptyView by default.

Remember to always inject your custom view factory in your view hierarchy:

var body: some Scene {
    WindowGroup {
        ChatChannelListView(viewFactory: CustomViewFactory.shared)
    }
}

Applying Custom Modifiers via Styles

List-level modifiers are part of the Styles protocol rather than ViewFactory. Implement the Styles protocol directly (the Styles classes are not open for subclassing) and override the methods you need. The Styles protocol provides default implementations for all modifier methods, so you only need to implement the required ones: composerPlacement, makeComposerInputViewModifier, makeComposerButtonViewModifier, and makeSuggestionsContainerModifier.

There are two channel list modifier methods available:

  • makeChannelListModifier — applied to the channel list itself.
  • makeChannelListContentModifier — applied to the channel list content, including both the header and footer views.
  • makeSearchableModifier — a modifier used for all the views that have search functionality (for example, the channel list).
class CustomStyles: Styles {
    var composerPlacement: ComposerPlacement = .docked

    func makeChannelListModifier(options: ChannelListModifierOptions) -> some ViewModifier {
        VerticalPaddingViewModifier()
    }

    func makeChannelListContentModifier(options: ChannelListContentModifierOptions) -> some ViewModifier {
        // Applied to the full content area including header and footer
        EmptyViewModifier()
    }

    func makeComposerInputViewModifier(options: ComposerInputModifierOptions) -> some ViewModifier {
        RegularInputViewModifier()
    }

    func makeComposerButtonViewModifier(options: ComposerButtonModifierOptions) -> some ViewModifier {
        RegularButtonViewModifier()
    }

    func makeSuggestionsContainerModifier(options: SuggestionsContainerModifierOptions) -> some ViewModifier {
        SuggestionsRegularContainerModifier()
    }
}

struct VerticalPaddingViewModifier: ViewModifier {
    public func body(content: Content) -> some View {
        content
            .padding(.vertical, 8)
    }
}

Then assign it on your factory:

class CustomFactory: ViewFactory {
    @Injected(\.chatClient) public var chatClient
    public var styles = CustomStyles()
    // ...
}

Customizing the Leading and Trailing Areas

When the user swipes right, the SDK by default shows two buttons – one for deleting the channel and one for showing more options. Customize this view by implementing makeTrailingSwipeActionsView in the ViewFactory:

public func makeTrailingSwipeActionsView(
    options: TrailingSwipeActionsViewOptions
) -> some View {
    CustomTrailingSwipeActionsView(
        channel: options.channel,
        offsetX: options.offsetX,
        buttonWidth: options.buttonWidth,
        leftButtonTapped: options.leftButtonTapped,
        rightButtonTapped: options.rightButtonTapped
    )
}

The TrailingSwipeActionsViewOptions provides:

  • channel – the channel being swiped.
  • offsetX – the current horizontal offset during the swipe.
  • buttonWidth – the fixed width available for each action button.
  • swipedChannelId – binding to the currently swiped channel id (set to nil to dismiss the swipe state).
  • leftButtonTapped – callback for the left trailing button.
  • rightButtonTapped – callback for the right trailing button.

To disable swiping to the right, return EmptyView from this method.

The leading area can be customized in a similar manner by implementing makeLeadingSwipeActionsView:

func makeLeadingSwipeActionsView(
    options: LeadingSwipeActionsViewOptions
) -> some View {
    HStack {
        ActionItemButton(imageName: "pin.fill") {
            options.buttonTapped(options.channel)
        }
        .frame(width: options.buttonWidth)
        .foregroundColor(Color.white)
        .background(Color.yellow)

        Spacer()
    }
}

The LeadingSwipeActionsViewOptions provides:

  • channel – the channel being swiped.
  • offsetX – the current horizontal offset.
  • buttonWidth – the fixed width available for each action button.
  • swipedChannelId – binding to the currently swiped channel id.
  • buttonTapped – callback for the leading swipe button.

Finally, inject your custom view factory in your view hierarchy.

var body: some Scene {
    WindowGroup {
        ChatChannelListView(viewFactory: CustomViewFactory.shared)
    }
}

Search Results View

Replace the search results view with your own implementation by implementing makeSearchResultsView:

func makeSearchResultsView(
    options: SearchResultsViewOptions
) -> some View {
    CustomSearchResultsView(
        factory: self,
        selectedChannel: options.selectedChannel,
        searchResults: options.searchResults,
        loadingSearchResults: options.loadingSearchResults,
        channelNaming: options.channelNaming,
        onSearchResultTap: options.onSearchResultTap,
        onItemAppear: options.onItemAppear
    )
}

The SearchResultsViewOptions provides:

  • selectedChannel – binding of the currently selected channel selection info.
  • searchResults – the list of search results.
  • loadingSearchResults – whether results are currently loading.
  • channelNaming – closure that returns the display name for a channel.
  • onSearchResultTap – callback when a search result is tapped.
  • onItemAppear – callback when a search result item appears (for pagination).

You can also customize only the individual search result items with makeChannelListSearchResultItem:

func makeChannelListSearchResultItem(
    options: ChannelListSearchResultItemOptions<ChannelDestination>
) -> some View {
    SearchResultItem(
        factory: self,
        searchResult: options.searchResult,
        channelName: options.channelName,
        onSearchResultTap: options.onSearchResultTap,
        channelDestination: options.channelDestination
    )
}

More Channel Actions View

When the user presses the more button on a channel list item, a view with actions about the channel is shown (muting, deleting and more). Provide your own implementation with:

func makeMoreChannelActionsView(
    options: MoreChannelActionsViewOptions
) -> some View {
    VStack {
        Text("This is our custom view")
        Spacer()
        HStack {
            Button {
                options.onDismiss()
            } label: {
                Text("Action")
            }
        }
        .padding()
    }
}

The MoreChannelActionsViewOptions provides:

  • channel – the channel for which actions are shown.
  • swipedChannelId – binding to the currently swiped channel id.
  • onDismiss – callback to dismiss the actions view.
  • onError – callback when an error occurs.

You can also customize only the presented actions via the supportedMoreChannelActions closure on ChannelListConfig:

let channelListConfig = ChannelListConfig(
    supportedMoreChannelActions: { options in
        var defaultActions = ChannelAction.defaultActions(for: options)

        let freeze: @MainActor () -> Void = {
            let controller = chatClient.channelController(for: options.channel.cid)
            controller.freezeChannel { error in
                if let error {
                    options.onError(error)
                } else {
                    options.onDismiss()
                }
            }
        }

        let channelAction = ChannelAction(
            title: "Freeze channel",
            iconName: "person.crop.circle.badge.minus",
            action: freeze,
            confirmationPopup: nil,
            isDestructive: false
        )

        defaultActions.insert(channelAction, at: 0)
        return defaultActions
    }
)
let utils = Utils(channelListConfig: channelListConfig)
let streamChat = StreamChat(chatClient: chatClient, utils: utils)