@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()
}SwiftUI Integration
The AI components work seamlessly with the StreamChatSwiftUI SDK.
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:
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.