SwiftUI Integration

The AI components work seamlessly with the StreamChatSwiftUI SDK. Add the StreamChatAI library, available from version 5.13.0 of the Stream Chat Swift SDK, next to StreamChatSwiftUI, as described in the installation section.

Streaming Message View

You can easily integrate the message streaming view into the StreamChatSwiftUI SDK.

One option is to add it as a custom attachment:

@ViewBuilder
func makeCustomAttachmentViewType(options: CustomAttachmentViewTypeOptions) -> some View {
    let message = options.message
    let isGenerating = message.extraData["generating"]?.boolValue == true
    StreamingMessageView(
        content: message.text,
        isGenerating: isGenerating
    )
    .padding()
}

If your agent stores its reasoning and tool calls on the reply, show them above the text in the same view:

@ViewBuilder
func makeCustomAttachmentViewType(options: CustomAttachmentViewTypeOptions) -> some View {
    let message = options.message
    let isGenerating = message.extraData["generating"]?.boolValue == true
    let parts = AIMessagePart.parts(
        from: message.allAttachments.map { ($0.type.rawValue, $0.payload) }
    )
    VStack(alignment: .leading, spacing: 8) {
        if !parts.isEmpty {
            AIMessagePartsView(parts: parts, approver: toolApprover)
        }
        StreamingMessageView(
            content: message.text,
            isGenerating: isGenerating
        )
    }
    .padding()
}

Here toolApprover is an AIToolApprover your view factory holds, which sends the user's answers to tool calls that wait for their approval. Leave it out if your agent doesn't ask.

Then, you need to create your own message resolver that will tell the SDK to treat the messages with "ai_generated" custom data as custom attachments:

class CustomMessageResolver: MessageTypeResolving {

    func hasCustomAttachment(message: ChatMessage) -> Bool {
        message.extraData["ai_generated"] == true
    }
}

Finally, you need to inject this resolver when initializing the SDK:

let utils = Utils(messageTypeResolver: CustomMessageResolver())
let streamChat = StreamChat(chatClient: chatClient, utils: utils)

You can also add thinking indicators, for example at the bottom of the message list, using the makeMessageListContainerModifier from the Styles protocol.

Styles is a protocol with several methods that have default implementations and three that do not (makeComposerInputViewModifier, makeComposerButtonViewModifier, and makeSuggestionsContainerModifier). The SDK ships two concrete implementations (RegularStyles and LiquidGlassStyles) but they are not declared open, so you cannot subclass them from your own module — instead, conform to Styles directly and implement the three required methods using the public RegularInputViewModifier, RegularButtonViewModifier, and SuggestionsRegularContainerModifier types (or the LiquidGlass variants):

final class CustomStyles: Styles {
    var composerPlacement: ComposerPlacement = .docked
    let typingIndicatorHandler: TypingIndicatorHandler

    init(typingIndicatorHandler: TypingIndicatorHandler) {
        self.typingIndicatorHandler = typingIndicatorHandler
    }

    func makeMessageListContainerModifier(
        options: MessageListContainerModifierOptions
    ) -> some ViewModifier {
        CustomMessageListContainerModifier(typingIndicatorHandler: typingIndicatorHandler)
    }

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

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

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

Assign it to your ViewFactory's styles property:

class CustomViewFactory: ViewFactory {
    @Injected(\.chatClient) public var chatClient
    let typingIndicatorHandler: TypingIndicatorHandler
    lazy var styles = CustomStyles(typingIndicatorHandler: typingIndicatorHandler)

    init(typingIndicatorHandler: TypingIndicatorHandler) {
        self.typingIndicatorHandler = typingIndicatorHandler
    }
    // ...
}

Where the CustomMessageListContainerModifier would look something like this:

struct CustomMessageListContainerModifier: ViewModifier {

    @ObservedObject var typingIndicatorHandler: TypingIndicatorHandler

    func body(content: Content) -> some View {
        content.overlay {
            AIAgentOverlayView(typingIndicatorHandler: typingIndicatorHandler)
        }
    }
}

struct AIAgentOverlayView: View {

    @ObservedObject var typingIndicatorHandler: TypingIndicatorHandler

    var body: some View {
        VStack {
            Spacer()
            if typingIndicatorHandler.typingIndicatorShown {
                HStack {
                    AITypingIndicatorView(text: typingIndicatorHandler.state)
                    Spacer()
                }
                .padding()
                .frame(height: 60)
                .background(Color(UIColor.secondarySystemBackground))
            }
        }
    }
}

The TypingIndicatorHandler can be an observable object that listens to the different events for the thinking state. For a reference implementation, please check our demo app.