# 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`](https://getstream.io/chat/docs/sdk/ios/ai-components/conversation-state/), 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:

```swift
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:

- `NSCameraUsageDescription` and `NSPhotoLibraryUsageDescription`, for the attachment picker.
- `NSMicrophoneUsageDescription` and `NSSpeechRecognitionUsageDescription`, 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:

```swift
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`:

```swift
@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`:

```swift
composerViewModel.isTextFieldFocused = true
```

### Chat 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:

```swift
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`:

```swift
AIComposerView(
    viewModel: composerViewModel,
    onMessageSend: { message in
        send(message)
    },
    onStopGenerating: {
        channelController
            .eventsController()
            .sendEvent(AIIndicatorStopEvent(cid: channelController.cid))
        composerViewModel.isGenerating = false
    }
)
```

[What your backend sends](https://getstream.io/chat/docs/sdk/ios/ai-components/conversation-state/#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:

```swift
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](https://getstream.io/chat/docs/sdk/ios/ai-components/use-cases/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:

```swift
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](https://getstream.io/chat/docs/sdk/ios/ai-components/#theming):

- **Colors:** `composer...` for the composer, such as `composerBackground` and `composerChatOptionBackground`, `attachmentPicker...` for the picker sheet, and `attachmentBadge...` for the selected photos.
- **Fonts:** `composerChatOption`, and `chatOptionTitle` and `chatOptionDescription` for the options in the picker sheet.
- **Icons:** `composerAddAttachment`, `composerSend`, `composerStopGenerating`, `composerStartDictation`, `composerStopDictation`, `composerRemove` and `attachmentPickerCamera`.

Its texts, such as the "Ask anything" placeholder, go through `AIAppearance.localizationProvider`, under the `composer.*` keys.

---

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