# Channel List Item

The channel list item is the view displayed for each channel inside `ChatChannelListView`. The SwiftUI SDK exposes three levels of customization for this channel item, from the broadest to the most granular. Pick the one that matches how much of the default UI you want to keep.

1. Replace the channel item entirely via the `ViewFactory`.
2. Subclass `ChatChannelListItemViewModel` to tweak presentation logic while keeping the default layout.
3. Compose your own channel item from the public sub-views (title, message preview, mute icon, attachment icon, ...).

## Replacing the Channel Item

To replace the channel item with your own view, implement `makeChannelListItem` in the `ViewFactory` protocol and return your custom view. The SDK ships two composable wrappers that you can stack around any channel item view to keep the standard behaviors:

- `ChatChannelNavigatableListItem` adds navigation handling: it pushes the configured channel destination when `selectedChannel` matches the current channel.
- `ChatChannelSwipeableListItem` adds the swipe-to-action gestures and trailing/leading action buttons.

Both wrappers accept any `View` via their `channelListItem:` parameter, so you can plug your own custom channel item view at the bottom of the stack:

```swift
public func makeChannelListItem(
    options: ChannelListItemOptions<ChannelDestination>
) -> some View {
    let customChannelItem = CustomChannelItemView(
        channel: options.channel,
        onTap: { options.onItemTap(options.channel) }
    )

    let navigatable = ChatChannelNavigatableListItem(
        channel: options.channel,
        channelListItem: customChannelItem,
        channelDestination: options.channelDestination,
        selectedChannel: options.selectedChannel
    )

    return ChatChannelSwipeableListItem(
        factory: self,
        channelListItem: navigatable,
        swipedChannelId: options.swipedChannelId,
        channel: options.channel,
        numberOfTrailingItems: options.channel.ownCapabilities.contains(.deleteChannel) ? 2 : 1,
        trailingRightButtonTapped: options.trailingSwipeRightButtonTapped,
        trailingLeftButtonTapped: options.trailingSwipeLeftButtonTapped,
        leadingSwipeButtonTapped: options.leadingSwipeButtonTapped
    )
}
```

Your custom channel item view is responsible for handling its own tap. Call `options.onItemTap(options.channel)` from its tap handler so the navigation wrapper above can react and push the destination. Each wrapper is optional: omit `ChatChannelNavigatableListItem` if your custom channel item drives navigation itself, or omit `ChatChannelSwipeableListItem` if you don't need swipe actions.

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`. The [Swipe Actions for the Channel List](/chat/docs/sdk/ios/swiftui/channel-list-components/swipe-actions-channels/) page covers 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.

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

## Customizing the View Model

`ChatChannelListItemViewModel` is an `open` class that owns the presentation logic for the default channel item. Properties that carry presentation logic are `open`, so you can subclass and override the individual ones you care about without forking the channel item layout.

Two of the most common properties to customize are the message preview text and the timestamp. The example below hides the message content behind a generic placeholder for unread channels and formats the timestamp with a custom date format:

```swift
final class CustomChannelListItemViewModel: ChatChannelListItemViewModel {
    /// Hide the message content in previews and show a placeholder instead.
    override var messagePreviewText: String {
        hasUnread ? "New message" : super.messagePreviewText
    }

    /// Use a custom time format for the timestamp.
    override var timestampText: String {
        guard let lastMessageAt = channel.lastMessageAt else { return "" }
        return Self.timeFormatter.string(from: lastMessageAt)
    }

    private static let timeFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm"
        return formatter
    }()
}
```

Each override still falls back to `super` for the cases it doesn't handle, so the rest of the default presentation logic stays intact.

To plug your view model into the channel list, use the `viewModel:` initializer on `ChatChannelListItem` from your `ViewFactory`:

```swift
public func makeChannelListItem(
    options: ChannelListItemOptions<ChannelDestination>
) -> some View {
    ChatChannelListItem(
        factory: self,
        viewModel: CustomChannelListItemViewModel(
            channel: options.channel,
            channelName: options.channelName
        ),
        isSelected: options.selectedChannel.wrappedValue?.channel.cid == options.channel.cid,
        disabled: options.disabled,
        onItemTap: options.onItemTap
    )
}
```

## Composing from existing subviews

When you need a custom channel item layout but still want the SDK's styling for individual pieces, use the public sub-views directly. The default `ChatChannelListItem` is composed of the following building blocks, each of which you can use on its own to re-layout the channel item:

- `ChannelAvatar`: the leading avatar (or stacked member avatars for group channels).
- `ChannelItemTitleView`: the channel name plus the optional inline mute icon.
- `BadgeNotificationView`: the trailing unread count badge.
- `MessageReadIndicatorView`: the read/delivered/sending status checkmark next to the preview.
- `ChannelItemPreviewView`: the second line that shows the latest activity (message, typing, draft, deleted, or failed to send).

Every sub-view takes primitive values (strings, booleans, images, counts) so it can be reused in any layout. You can drive them yourself or let `ChatChannelListItemViewModel` produce the right values for each channel.

### Channel Item Preview

The preview is the second line of the channel item. It shows the most recent activity in the channel: the latest message (with author prefix, attachment glyph, or deleted placeholder), a pending draft, a typing indicator, or a "failed to send" status.

`ChannelItemPreviewView` is the view that displays the preview. The variant it shows is described by a `ChannelItemPreview` value, built via one of its static factory methods. Each method takes the content value for that variant:

```swift
ChannelItemPreviewView(preview: .failedToSend(.init()))
ChannelItemPreviewView(preview: .typing(.init(channel: channel)))
ChannelItemPreviewView(preview: .draft(.init(text: "Hi there")))
ChannelItemPreviewView(preview: .deleted(.init(isSentByCurrentUser: true)))
ChannelItemPreviewView(preview: .message(.init(text: "Last message")))
```

Each built-in variant has its own content type nested under `ChannelItemPreview`:

- `FailedToSendContent`
- `TypingContent`
- `DraftContent`
- `DeletedContent`
- `MessageContent`

The actual content is stored on `ChannelItemPreview.content`, typed as `any ChannelItemPreview.Content`. To render your own view per variant, switch on it and match the concrete content type:

```swift
switch viewModel.preview.content {
case let draft as ChannelItemPreview.DraftContent:
    Text(draft.text).italic()
case is ChannelItemPreview.TypingContent:
    Text("typing...")
default:
    ChannelItemPreviewView(preview: viewModel.preview)
}
```

If you only need one specific variant, each variant is also exposed as its own `View`:

- `ChannelItemFailedToSendView`
- `ChannelItemDraftPreviewView`
- `ChannelItemDeletedPreviewView`
- `ChannelItemMessagePreviewView`
- `SubtitleTypingIndicatorView`

### Composing with the View Model

`ChatChannelListItemViewModel` already exposes everything the building blocks above need (avatar channel, title, unread state, read users, preview variant, ...). The view model is not observable and performs no extra work on its own, so you can build it inline and feed its values into your own layout.

The example below rearranges the default pieces into a compact two-line item using the view model to drive the data:

```swift
struct CompactChannelItemView: View {
    let viewModel: ChatChannelListItemViewModel
    let isSelected: Bool

    var body: some View {
        HStack(spacing: 12) {
            ChannelAvatar(channel: viewModel.channel, size: 48)

            VStack(alignment: .leading, spacing: 2) {
                HStack {
                    ChannelItemTitleView(
                        channelName: viewModel.channelName,
                        showInlineMutedIcon: viewModel.showInlineMutedIcon
                    )
                    Spacer()
                    Text(viewModel.timestampText)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                    if !isSelected && viewModel.hasUnread {
                        BadgeNotificationView(count: viewModel.unreadCount)
                    }
                }

                HStack(spacing: 4) {
                    if viewModel.showReadEvents {
                        MessageReadIndicatorView(
                            readUsers: viewModel.readUsers,
                            showDelivered: viewModel.showDelivered,
                            localState: viewModel.previewMessageLocalState
                        )
                    }
                    ChannelItemPreviewView(preview: viewModel.preview)
                }
            }
        }
    }
}
```

Combine this with the `makeChannelListItem` factory override from [Replacing the Channel Item](#replacing-the-channel-item) to plug your custom channel item view into the channel list, and with a `CustomChannelListItemViewModel` subclass to tweak the data it displays.


---

This page was last updated at 2026-06-05T14:24:22.851Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/ios/swiftui/channel-list-components/channel-list-item/](https://getstream.io/chat/docs/sdk/ios/swiftui/channel-list-components/channel-list-item/).