class CustomFactory: ViewFactory {
@Injected(\.chatClient) public var chatClient
public var styles = RegularStyles()
private init() {}
public static let shared = CustomFactory()
func makeUserAvatarView(options: UserAvatarViewOptions) -> some View {
CustomUserAvatarView(
user: options.user,
size: options.size
)
}
}Customizing Components
The SwiftUI SDK components are fully customizable and interchangeable through the ViewFactory protocol that holds all the reusable views of the SDK. You can find the full reference of the ViewFactory protocol here.
View Factory
The ViewFactory protocol defines the swappable views of the chat experience. There are default implementations for all the views used in the SDK. If you want to customize a view, you will need to implement the ViewFactory protocol, but only implement the view you want to swap.
In most cases, your custom view only needs to conform to the standard SwiftUI View protocol and provide a body property, there's no need to implement additional lifecycle methods or protocols. The only exception is when customizing the navigation bar, where your custom view must conform to SwiftUI's ToolbarContent protocol.
Options-Based API
All ViewFactory methods accept a single Options struct instead of individual parameters. Each Options struct groups together all the data and callbacks needed to create that view. This design makes it easy to add new parameters in future releases without breaking your existing implementations.
For example, if you want to change the default user avatar view that is used in multiple views, you implement the makeUserAvatarView(options:) slot in the ViewFactory protocol:
Afterwards, you need to inject the CustomFactory in your view. To do this, you pass the newly created factory to the ChatChannelListView. If you are only using the ChatChannelView, you can pass the factory to it instead.
var body: some Scene {
WindowGroup {
ChatChannelListView(viewFactory: CustomFactory.shared)
}
}And that's everything needed to provide custom views for the SwiftUI SDK.
The ViewFactory protocol is generic over the views that it can replace, and there is no usage of AnyView. This allows SwiftUI to compute the diffing of the views faster and more accurately without any performance overhead.
Styles
In addition to ViewFactory, the SDK provides a Styles protocol that controls the overall look and layout of key UI surfaces. Every ViewFactory has a styles property that you can set to control styling independently from view swapping.
The two built-in implementations are:
| Style | Composer placement | Look |
|---|---|---|
RegularStyles | Docked at the bottom | Classic chat UI |
LiquidGlassStyles | Floating above keyboard | iOS 26 Liquid Glass |
To use a particular style, set the styles property on your factory:
class CustomFactory: ViewFactory {
@Injected(\.chatClient) public var chatClient
public var styles = LiquidGlassStyles()
private init() {}
public static let shared = CustomFactory()
}You can also create your own custom Styles implementation to override specific parts:
class CustomStyles: Styles {
var composerPlacement: ComposerPlacement = .docked
func makeComposerInputViewModifier(options: ComposerInputModifierOptions) -> some ViewModifier {
MyInputModifier()
}
func makeComposerButtonViewModifier(options: ComposerButtonModifierOptions) -> some ViewModifier {
MyButtonModifier()
}
func makeScrollToBottomButtonModifier(options: ScrollToBottomButtonModifierOptions) -> some ViewModifier {
MyScrollToBottomModifier()
}
func makeComposerViewModifier(options: ComposerViewModifierOptions) -> some ViewModifier {
MyComposerBackgroundModifier()
}
func makeSuggestionsContainerModifier(options: SuggestionsContainerModifierOptions) -> some ViewModifier {
MySuggestionsModifier()
}
func makeSearchableModifier(options: SearchableModifierOptions) -> some ViewModifier {
DefaultSearchableModifier(
searchText: options.searchText,
prompt: "Search"
)
}
}The full list of Styles hooks includes:
makeComposerInputViewModifier– modifier applied to the composer text input area.makeComposerButtonViewModifier– modifier applied to composer action buttons.makeScrollToBottomButtonModifier– modifier applied to the scroll-to-bottom button.makeComposerViewModifier– modifier applied to the whole composer container.makeSuggestionsContainerModifier– modifier applied to the suggestions overlay.makeChannelListContentModifier– modifier for the full channel list content area.makeChannelListModifier– modifier applied to the channel list itself.makeSearchableModifier– modifier applied to built-in search UI such as the channel list search.makeMessageListModifier– modifier applied to the message list.makeMessageListContainerModifier– modifier applied to the message list container.makeMessageViewModifier– modifier applied to each individual message bubble.makeBouncedMessageActionsModifier– modifier for bounced-message action UI.
Complete ViewFactory reference
Below is the full list of ViewFactory hooks, grouped by area. Expand a section to see all available slots. You can also browse the full source here.
Channel List
| Method | Description |
|---|---|
func makeChannelListHeaderViewModifier(options: ChannelListHeaderViewModifierOptions) -> HeaderViewModifier | Navigation header view modifier for the channel list |
func makeNoChannelsView(options: NoChannelsViewOptions) -> NoChannels | Empty state view when there are no channels |
func makeLoadingView(options: LoadingViewOptions) -> LoadingContent | Loading state view for the channel list |
func makeChannelListItem(options: ChannelListItemOptions<ChannelDestination>) -> ChannelListItemType | View for each channel row in the list |
func makeChannelAvatarView(options: ChannelAvatarViewOptions) -> ChannelAvatarViewType | Avatar shown in the channel list, header, and search results |
func makeChannelListBackground(options: ChannelListBackgroundOptions) -> ChannelListBackground | Background view for the channel list |
func makeChannelListItemBackground(options: ChannelListItemBackgroundOptions) -> ChannelListItemBackground | Background view for each channel list item |
func makeChannelListDividerItem(options: ChannelListDividerItemOptions) -> ChannelListDividerItem | Divider between channel list items |
func makeMoreChannelActionsView(options: MoreChannelActionsViewOptions) -> MoreActionsView | Additional channel actions popup |
func makeTrailingSwipeActionsView(options: TrailingSwipeActionsViewOptions) -> TrailingSwipeActionsViewType | Trailing swipe actions for a channel row |
func makeLeadingSwipeActionsView(options: LeadingSwipeActionsViewOptions) -> LeadingSwipeActionsViewType | Leading swipe actions for a channel row |
func makeChannelListTopView(options: ChannelListTopViewOptions) -> ChannelListTopViewType | View shown above the channel list |
func makeChannelListFooterView(options: ChannelListFooterViewOptions) -> ChannelListFooterViewType | Footer view for the channel list |
func makeChannelListStickyFooterView(options: ChannelListStickyFooterViewOptions) -> ChannelListStickyFooterViewType | Sticky footer view for the channel list |
func makeSearchResultsView(options: SearchResultsViewOptions) -> ChannelListSearchResultsViewType | Container view for channel/message search results |
func makeChannelListSearchResultItem(options: ChannelListSearchResultItemOptions<ChannelDestination>) -> ChannelListSearchResultItem | View for each search result item |
Channel And Message List
| Method | Description |
|---|---|
func makeChannelDestination(options: ChannelDestinationOptions) -> @MainActor (ChannelSelectionInfo) -> ChannelDestination | Destination view when navigating to a channel |
func makeMessageThreadDestination(options: MessageThreadDestinationOptions) -> @MainActor (ChatChannel, ChatMessage) -> MessageThreadDestination | Destination view when navigating to a message thread |
func makeEmptyMessagesView(options: EmptyMessagesViewOptions) -> EmptyMessagesViewType | Empty state view when there are no messages |
func makeUserAvatarView(options: UserAvatarViewOptions) -> UserAvatarViewType | User avatar used throughout the SDK |
func makeChannelHeaderViewModifier(options: ChannelHeaderViewModifierOptions) -> ChatHeaderViewModifier | Navigation header modifier for the channel view |
func makeChannelBarsVisibilityViewModifier(options: ChannelBarsVisibilityViewModifierOptions) -> ChangeBarsVisibilityModifier | Controls top/bottom bars visibility |
func makeChannelLoadingView(options: ChannelLoadingViewOptions) -> ChannelLoadingViewType | Loading state for ChatChannelView |
func makeMessageThreadHeaderViewModifier(options: MessageThreadHeaderViewModifierOptions) -> ThreadHeaderViewModifier | Navigation header modifier for the thread view |
func makeMessageListBackground(options: MessageListBackgroundOptions) -> MessageListBackground | Background view for the message list |
func makeMessageItemView(options: MessageItemViewOptions) -> MessageItemViewType | Container view for each message in the list |
func makeMessageTextView(options: MessageTextViewOptions) -> MessageTextViewType | Text content view for a message |
func makeMessageDateView(options: MessageDateViewOptions) -> MessageDateViewType | Date label shown below/inside a message |
func makeMessageAuthorAndDateView(options: MessageAuthorAndDateViewOptions) -> MessageAuthorAndDateViewType | Author name and date header layout |
func makeMessageTopView(options: MessageTopViewOptions) -> MessageTopViewType | View shown above the message bubble |
func makeLastInGroupHeaderView(options: LastInGroupHeaderViewOptions) -> LastInGroupHeaderView | Header view for the last message in a group |
func makeMessageAttachmentsView(options: MessageAttachmentsViewOptions) -> MessageAttachmentsViewType | Container view for message attachments |
func makeImageAttachmentView(options: ImageAttachmentViewOptions) -> ImageAttachmentViewType | Image attachment view |
func makeGiphyAttachmentView(options: GiphyAttachmentViewOptions) -> GiphyAttachmentViewType | Giphy attachment view |
func makeLinkAttachmentView(options: LinkAttachmentViewOptions) -> LinkAttachmentViewType | Link preview attachment view |
func makeFileAttachmentView(options: FileAttachmentViewOptions) -> FileAttachmentViewType | File attachment view |
func makeVideoAttachmentView(options: VideoAttachmentViewOptions) -> VideoAttachmentViewType | Video attachment view |
func makeMediaViewer(options: MediaViewerOptions) -> MediaViewerType | Full-screen media gallery viewer |
func makeMediaViewerHeader(options: MediaViewerHeaderOptions) -> MediaViewerHeaderType | Header for the media gallery viewer |
func makeVideoPlayerView(options: VideoPlayerViewOptions) -> VideoPlayerViewType | Video player view |
func makeVideoPlayerHeaderView(options: VideoPlayerHeaderViewOptions) -> VideoPlayerHeaderViewType | Header for the video player |
func makeVideoPlayerFooterView(options: VideoPlayerFooterViewOptions) -> VideoPlayerFooterViewType | Footer for the video player |
func makeDeletedMessageView(options: DeletedMessageViewOptions) -> DeletedMessageViewType | View for deleted messages |
func makeSystemMessageView(options: SystemMessageViewOptions) -> SystemMessageViewType | View for system messages |
func makeEmojiTextView(options: EmojiTextViewOptions) -> EmojiTextViewType | Text view that renders emoji |
func makeVoiceRecordingView(options: VoiceRecordingViewOptions) -> VoiceRecordingViewType | Voice recording attachment view |
func makeCustomAttachmentViewType(options: CustomAttachmentViewTypeOptions) -> CustomAttachmentViewType | View for custom attachment types |
func makeScrollToBottomButton(options: ScrollToBottomButtonOptions) -> ScrollToBottomButtonType | Button to scroll to the latest message |
func makeDateIndicatorView(options: DateIndicatorViewOptions) -> DateIndicatorViewType | Date overlay/separator indicator |
func makeMessageListDateIndicator(options: MessageListDateIndicatorViewOptions) -> MessageListDateIndicatorViewType | Date indicator shown while scrolling |
func makeInlineTypingIndicatorView(options: TypingIndicatorViewOptions) -> InlineTypingIndicatorViewType | Inline typing indicator shown in the message list |
func makeSubtitleTypingIndicatorView(options: SubtitleTypingIndicatorViewOptions) -> SubtitleTypingIndicatorViewType | Subtitle typing indicator in the navigation bar |
func makeGiphyBadgeViewType(options: GiphyBadgeViewTypeOptions) -> GiphyBadgeViewType | Badge overlay for Giphy messages |
func makeMessageRepliesView(options: MessageRepliesViewOptions) -> MessageRepliesViewType | Thread replies count button below a message |
Composer And Suggestions
| Method | Description |
|---|---|
func makeMessageComposerViewType(options: MessageComposerViewTypeOptions) -> MessageComposerViewType | The full message composer component |
func makeLeadingComposerView(options: LeadingComposerViewOptions) -> LeadingComposerViewType | Leading view of the composer (e.g. add attachment button) |
func makeComposerInputView(options: ComposerInputViewOptions) -> ComposerInputViewType | Input area of the composer |
func makeComposerTextInputView(options: ComposerTextInputViewOptions) -> ComposerTextInputViewType | Text input field inside the composer |
func makeComposerInputTrailingView(options: ComposerInputTrailingViewOptions) -> ComposerInputTrailingViewType | Trailing view inside the composer input (e.g. emoji button) |
func makeTrailingComposerView(options: TrailingComposerViewOptions) -> TrailingComposerViewType | Trailing view of the composer (e.g. send button) |
func makeSendMessageButton(options: SendMessageButtonOptions) -> SendMessageButtonType | Send message button |
func makeConfirmEditButton(options: ConfirmEditButtonOptions) -> ConfirmEditButtonType | Confirm button when editing a message |
func makeComposerVoiceRecordingInputView(options: ComposerVoiceRecordingInputViewOptions) -> ComposerVoiceRecordingInputViewType | Voice recording UI inside the composer |
func makeAttachmentPickerView(options: AttachmentPickerViewOptions) -> AttachmentPickerViewType | Attachment picker container |
func makeAttachmentCommandsPickerView(options: AttachmentCommandsPickerViewOptions) -> AttachmentCommandsPickerViewType | Commands picker in the attachment area |
func makeAttachmentTypePickerView(options: AttachmentTypePickerViewOptions) -> AttachmentTypePickerViewType | Attachment type selector (photo, file, poll, etc.) |
func makeAttachmentMediaPickerView(options: AttachmentMediaPickerViewOptions) -> AttachmentMediaPickerViewType | Photo/video picker |
func makeAttachmentFilePickerView(options: AttachmentFilePickerViewOptions) -> AttachmentFilePickerViewType | File picker |
func makeAttachmentCameraPickerView(options: AttachmentCameraPickerViewOptions) -> AttachmentCameraPickerViewType | Camera picker |
func makeCustomAttachmentPickerView(options: CustomAttachmentPickerViewOptions) -> CustomAttachmentPickerViewType | Custom attachment picker |
func makeCustomAttachmentPreviewView(options: CustomAttachmentPreviewViewOptions) -> CustomAttachmentPreviewViewType | Preview for custom attachments in the composer |
func makeAttachmentPollPickerView(options: AttachmentPollPickerViewOptions) -> AttachmentPollPickerViewType | Poll creation picker in the composer |
func makeSendInChannelView(options: SendInChannelViewOptions) -> SendInChannelViewType | "Send also in channel" toggle for thread replies |
func makeSuggestionsContainerView(options: SuggestionsContainerViewOptions) -> SuggestionsContainerViewType | Container for command/mention suggestions |
func makeComposerQuotedMessageView(options: ComposerQuotedMessageViewOptions) -> ComposerQuotedMessageViewType | Quoted message shown inside the composer |
func makeQuotedMessageView(options: QuotedMessageViewOptions) -> QuotedMessageViewType | Base quoted message view |
func makeChatQuotedMessageView(options: ChatQuotedMessageViewOptions) -> ChatQuotedMessageViewType | Quoted message shown in the message list |
func makeComposerEditedMessageView(options: ComposerEditedMessageViewOptions) -> ComposerEditedMessageViewType | Edited message shown inside the composer |
func makeMessageAttachmentPreviewThumbnailView(options: MessageAttachmentPreviewViewOptions) -> MessageAttachmentPreviewViewType | Thumbnail preview for attachments in the composer |
func makeMessageAttachmentPreviewIconView(options: MessageAttachmentPreviewIconViewOptions) -> MessageAttachmentPreviewIconViewType | Icon preview for attachments in the composer |
Reactions, Actions, And Read State
| Method | Description |
|---|---|
func makeMessageActionsView(options: MessageActionsViewOptions) -> MessageActionsViewType | Message actions popup (copy, delete, edit, etc.) |
func makeReactionsDetailView(options: ReactionsDetailViewOptions) -> ReactionsDetailViewType | Detail view showing who reacted |
func makeBottomReactionsView(options: ReactionsBottomViewOptions) -> ReactionsBottomViewType | Reactions shown below a message |
func makeMessageReactionView(options: MessageReactionViewOptions) -> MessageReactionViewType | Single reaction view |
func makeReactionsOverlayView(options: ReactionsOverlayViewOptions) -> ReactionsOverlayViewType | Overlay shown when adding a reaction |
func makeReactionsBackgroundView(options: ReactionsBackgroundOptions) -> ReactionsBackground | Background view behind the reactions overlay |
func makeReactionsContentView(options: ReactionsContentViewOptions) -> ReactionsContentView | Content view for the reactions picker |
func makeMoreReactionsView(options: MoreReactionsViewOptions) -> MoreReactionsViewType | "More reactions" button/view |
func makeMessageReadIndicatorView(options: MessageReadIndicatorViewOptions) -> MessageReadIndicatorViewType | Read indicator shown next to messages |
func makeNewMessagesIndicatorView(options: NewMessagesIndicatorViewOptions) -> NewMessagesIndicatorViewType | Banner indicating new messages arrived |
func makeJumpToUnreadButton(options: JumpToUnreadButtonOptions) -> JumpToUnreadButtonType | Button to jump to the first unread message |
Polls
| Method | Description |
|---|---|
func makeComposerPollView(options: ComposerPollViewOptions) -> ComposerPollViewType | Poll creation view in the composer |
func makePollView(options: PollViewOptions) -> PollViewType | Poll attachment view in the message list |
Thread List
| Method | Description |
|---|---|
func makeThreadDestination(options: ThreadDestinationOptions) -> @MainActor (ChatThread) -> ThreadDestination | Destination view when navigating to a thread |
func makeThreadListItem(options: ThreadListItemOptions<ThreadDestination>) -> ThreadListItemType | View for each thread row in the thread list |
func makeNoThreadsView(options: NoThreadsViewOptions) -> NoThreads | Empty state view when there are no threads |
func makeThreadListLoadingView(options: ThreadListLoadingViewOptions) -> ThreadListLoadingView | Loading state view for the thread list |
func makeThreadListContainerViewModifier(options: ThreadListContainerModifierOptions) -> ThreadListContainerModifier | Modifier applied to the thread list container |
func makeThreadListHeaderViewModifier(options: ThreadListHeaderViewModifierOptions) -> ThreadListHeaderViewModifier | Navigation header modifier for the thread list |
func makeThreadListHeaderView(options: ThreadListHeaderViewOptions) -> ThreadListHeaderView | Header view shown at the top of the thread list |
func makeThreadListFooterView(options: ThreadListFooterViewOptions) -> ThreadListFooterView | Footer shown when loading more threads |
func makeThreadListBackground(options: ThreadListBackgroundOptions) -> ThreadListBackground | Background view for the thread list |
func makeThreadListItemBackground(options: ThreadListItemBackgroundOptions) -> ThreadListItemBackground | Background view for each thread list item |
func makeThreadListDividerItem(options: ThreadListDividerItemOptions) -> ThreadListDividerItem | Divider between thread list items |
Add Users And Text Rendering
| Method | Description |
|---|---|
func makeAddUsersView(options: AddUsersViewOptions) -> AddUsersViewType | Add users view in the message composer |
func makeAttachmentTextView(options: AttachmentTextViewOptions) -> AttachmentTextViewType | Text preview for attachments |
func makeStreamTextView(options: StreamTextViewOptions) -> StreamTextViewType | Custom text view for message rendering |
Accessing Dependencies
When building your own view components and you want to use the dependencies of the SwiftUI SDK, you can access them by using the @Injected(\.keyPath) property wrapper:
@Injected(\.chatClient) var chatClient
@Injected(\.colors) var colors
@Injected(\.fonts) var fonts
@Injected(\.images) var images
@Injected(\.utils) var utilsThe @Injected property wrapper works similarly to the @Environment in SwiftUI, but it also allows access to the dependencies in non-view related code. This approach is inspired by the following Dependency Injection article by Antoine van der Lee.
Extending with Custom Types
In some cases, you might also need to extend the @Injected property wrapper with your own types. For example, you may want to be able to access your custom types like this:
@Injected(\.customType) var customTypeIn order to achieve this, you first need to define your own InjectionKey, and define it's currentValue, which basically creates the new instance of your type.
class CustomType {
// your custom logic here
}
struct CustomInjectionKey: InjectionKey {
static var currentValue: CustomType = CustomType()
}Next, you need to extend our InjectedValues with your own custom type, by defining its getter and setter.
extension InjectedValues {
/// Provides access to the `CustomType` instance in the views and view models.
var customType: CustomType {
get {
Self[CustomInjectionKey.self]
}
set {
Self[CustomInjectionKey.self] = newValue
}
}
}With these few simple steps, you can now access your custom type in both your app code and in your custom implementations of the views used throughout the SDK.
Build Time Improvements
If you are customizing many view slots (over 15) from the SDK, and you have many generics in your codebase, it's a good idea to explicitly specify the types of your custom views with typealias. This will improve the build time of your project.
For example, in our ViewFactory, the associatedType for creating the no channels view is NoChannels:
associatedtype NoChannels: View
func makeNoChannelsView(options: NoChannelsViewOptions) -> NoChannelsIn order to improve the build time, you would need to specify the associated type with a typealias, like this:
typealias NoChannels = CustomNoChannelsViewThen, your custom implementation of the factory method makeNoChannelsView will look like the following:
public func makeNoChannelsView(options: NoChannelsViewOptions) -> CustomNoChannelsView {
CustomNoChannelsView()
}You can find all the associated types we use in the ViewFactory here.