Composer
AIComposerView is where the user writes to your agent. It has a text field that grows with the text, photos from the library or the camera, a microphone that dictates into the field, and chat options such as a "deep research" mode. Give it an AIConversation, and it sends to the conversation, turns its send button into a stop button while the agent answers, and edits the user's latest message:
AIConversationView(conversation: conversation)
.safeAreaInset(edge: .bottom) {
AIComposerView(conversation: conversation)
}On iOS 15, its text field is a single line, and the attachment picker has no "All photos" button.
Usage descriptions
The attachment picker uses the camera and the photo library, and dictation uses the microphone and speech recognition. Add the matching keys to your Info.plist, each with a description of why your app needs it:
NSCameraUsageDescriptionandNSPhotoLibraryUsageDescription, for the attachment picker.NSMicrophoneUsageDescriptionandNSSpeechRecognitionUsageDescription, for dictation.
Send without a conversation
Without an AIConversation, pass a closure. It receives a MessageData, with the text, the attachment URLs and the selected chat option, every time the user taps send:
AIComposerView { message in
send(message)
}The composer clears its text and attachments once a message is sent. It keeps the temporary files it made for the attached photos, since the sent message may still be uploading them, and the system removes them later.
Control it from your code
To prefill the text, focus the field or offer chat options, pass your own AIComposerViewModel:
@StateObject private var composerViewModel = AIComposerViewModel()
AIComposerView(viewModel: composerViewModel) { message in
send(message)
}With a conversation, give the view model to the conversation instead, as AIConversation(transport:composerViewModel:), and the composer shows it.
To focus the text field, or dismiss the keyboard, set isTextFieldFocused:
composerViewModel.isTextFieldFocused = trueChat options
The view model's chatOptions, such as a "deep research" mode, are listed in the attachment picker sheet, and tapping one calls its action. Set the option as the view model's selectedChatOption there, to show it as a chip in the composer and send it with the message:
let research = ChatOption(
id: "research",
title: "Deep research",
description: "Takes longer, and checks more sources",
icon: "magnifyingglass", // an SF Symbol
shortTitle: "Research"
)
var option = research
option.action = { [weak composerViewModel] in
composerViewModel?.selectedChatOption = research
}
composerViewModel.chatOptions = [option]The selected option reaches your transport as the request's chatOption, and your closure as the message's chatOption. The user removes it from the chip.
Stop the reply
While the view model's isGenerating is true, the composer shows a stop button in place of the send button, and calls onStopGenerating when the user taps it. With a conversation, the composer does this for you.
Without one, set isGenerating while your agent writes a reply, and stop the agent when the button is tapped. With a Stream Chat channel, send it an AIIndicatorStopEvent:
AIComposerView(
viewModel: composerViewModel,
onMessageSend: { message in
send(message)
},
onStopGenerating: {
channelController
.eventsController()
.sendEvent(AIIndicatorStopEvent(cid: channelController.cid))
composerViewModel.isGenerating = false
}
)What your backend sends covers how the backend stops the reply.
Dictation
SpeechToTextButton turns speech into text with Apple's Speech framework. The composer shows one inside its text field while the field is empty, which dictates into it. To use the button in an input of your own, pass a closure that receives the transcript recognized so far:
SpeechToTextButton(locale: Locale(identifier: "en-US")) { transcript in
text = transcript
}When tapped, it asks for microphone access and records. Recording stops after silenceTimeout seconds of silence (3 by default), or when the user taps the button again.
To let the user talk to your agent instead, rather than dictate, see the Voice agent use case.
Customize the composer
AIComposerView takes a viewFactory that conforms to AIComposerViewFactory. The protocol has five slots, and each one has a default, so you override only the ones you want to change:
| Slot | Factory method | Default |
|---|---|---|
| Left of the text field | makeLeadingComposerView(options:) |
AddAttachmentsButton |
| The text field area | makeComposerInputView(options:) |
AIComposerInputView |
| Inside the text field, while it is empty | makeComposerInputTrailingView(options:) |
SpeechToTextButton |
| Right of the text field | makeTrailingComposerView(options:) |
EmptyView |
| Attachment picker sheet | makeComposerPickerView(options:) |
Built-in photo/camera picker |
For example, this factory replaces the attachment button with a paperclip and leaves dictation out:
final class CustomComposerFactory: AIComposerViewFactory {
func makeLeadingComposerView(options: AIComposerLeadingViewOptions) -> some View {
Button {
options.onTap()
} label: {
Image(systemName: "paperclip")
.padding(10)
.background(.ultraThinMaterial, in: Circle())
}
}
func makeComposerInputTrailingView(options: AIComposerInputTrailingViewOptions) -> some View {
EmptyView()
}
}
AIComposerView(conversation: conversation, viewFactory: CustomComposerFactory())Override makeTrailingComposerView(options:) to add a mode toggle, a slash-command trigger, or another control next to the field.
The factory is @MainActor, like SwiftUI views. Its option types have public initializers, so a custom slot can build the default views itself, for example an AIComposerInputView with a trailing view of your own, and send messages through the onMessageSend closure it receives.
Appearance
The composer reads its colors, fonts and icons from AIAppearance, as described in Theming:
- Colors:
composer...for the composer, such ascomposerBackgroundandcomposerChatOptionBackground,attachmentPicker...for the picker sheet, andattachmentBadge...for the selected photos. - Fonts:
composerChatOption, andchatOptionTitleandchatOptionDescriptionfor the options in the picker sheet. - Icons:
composerAddAttachment,composerSend,composerStopGenerating,composerStartDictation,composerStopDictation,composerRemoveandattachmentPickerCamera.
Its texts, such as the "Ask anything" placeholder, go through AIAppearance.localizationProvider, under the composer.* keys.