# ExyteChat: a SwiftUI chat view that never sends anything itself

> ExyteChat renders a chat interface and hands you a `DraftMessage` when the user taps send, leaving every network call to your own view model. Its documented content includes live location shares of 15 minutes, 1 hour or 8 hours.

**exyte/Chat** — A SwiftUI Chat UI framework with fully customizable message cells and a built-in media picker

- Repository: https://github.com/exyte/Chat
- Stars: 1,893 · Forks: 341
- Language: Swift
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/exyte-chat

## The send path is a closure, and that is the whole contract

The usage example is four lines of Swift and they explain what the library is:

```swift
@State var messages: [Message] = []

var body: some View {
    ChatView(messages: messages) { draft in
        yourViewModel.send(draft: draft)
    }
}
```

`messages` is the list to display. The trailing closure is called when the user presses send, and what it receives is a `DraftMessage`. The README states the mechanism plainly in the features list: sending means calling a closure, since the user will be the one providing actual API calls.

So this is a view layer, not a chat stack. There is no socket, no HTTP client, no retry logic and no local database in what is described. You supply `[Message]`, you receive a `DraftMessage`, and everything between those two points is yours.

The README also tells you how to relate their model to yours. `Message` is the type Chat uses internally, you map it both ways to your own `Message` model that your API expects, or you use it as is. That mapping step is real work if your domain model differs, and it is the part that determines how much value the library saves you.

One detail in the type design is worth noticing. `Message` stores its text in an `AttributedString`. Pass your own `AttributedString` or a plain `String`, in which case the initialiser applies default attributes of markdown and underlined links.

## Eight content kinds and the enum that carries them

The features list enumerates what a message can contain in any combination.

The list: arbitrarily styled text with `AttributedString` or markdown, photo and video, audio recording, a link with preview, gif or sticker, documents of any file type picked via the system document picker, location, and a custom dictionary of any `Sendable`.

That last item is the extensibility mechanism. A custom dictionary of any `Sendable` means the type can carry a payload this library knows nothing about, which is how you add a reaction, a poll, a product card or anything your product needs without forking.

Location is the item with the most surprising semantics. A static location is one thing, but a live location share keeps updating, and the README names three durations: 15 minutes, 1 hour, or 8 hours. The README documents those three supported durations.

Media picking is built in rather than delegated. The library has a photo and video library and camera picker for multiple media asset selection, and documents come through the system document picker rather than a custom one, which is the right call for sandbox reasons.

Audio recording and a Giphy sticker keyboard are also included. The built-in sticker keyboard integrates with Giphy.

## Chat type and reply mode are the two layout decisions

Two enumerations control how the view behaves, and both are passed to the same initialiser.

Chat type determines the order of messages and the direction of the new message animation. `conversation` puts the latest message at the bottom with new messages appearing from the bottom. `comments` puts the latest message at the top with new messages appearing from the top, which is the inverted layout an issue tracker or a comment stream needs.

Reply mode determines how replying looks. `quote` puts the new message at the newest position, quoting message A in its body. `answer` places the new message directly below message A as a separate cell, without duplicating A in its body.

Both together:

```swift
ChatView(messages: viewModel.messages, chatType: .comments, replyMode: .answer) { draft in
    yourViewModel.send(draft: draft)
}
```

Two features interact with the layout. Pagination is supported for loading older messages, and a long press on a message cell shows a fullscreen menu, which automatically shows scroll for big messages. That second behaviour is a detail that saves real work: a long message in a modal menu would otherwise be unreadable, and the library handles the scroll for you. Reply, edit and delete are all reachable through that menu.

## Custom cells take single-struct parameters as of version 3

Custom message rendering is done through a `messageBuilder` closure, and the shape of its argument changed in version 3.

The example shows the pattern, building a cell from a `params` value that exposes `message` and its attachments:

```swift
ChatView(messages: viewModel.messages) { draft in
    viewModel.send(draft: draft)
} messageBuilder: { params in
    let message = params.message
    VStack {
        Text(message.attributedText)
        if !message.attachments.isEmpty {
            ForEach(message.attachments, id: \.id) { at in
                AsyncImage(url: at.thumb
```

The example is truncated in the README, but the visible part shows the contract: a SwiftUI view you return, built from whatever state you want, including `AsyncImage` for remote thumbnails. `messageBuilder` and `inputViewBuilder` are both documented, so the input bar is customisable on the same terms.

Now the migration notes, because they are the compatibility information. Five changes land in version 3.

`enableLoadMore(pageSize...)` was renamed `enableLoadMore(offset...)`, and its trailing closure no longer takes any arguments. `linkPreviewsDisabled` was refactored into `linkPreviewsEnabled`, which inverts the sense of the flag. `shouldShowLinkPreview` was renamed `shouldShowPreviewForLink`. `messageUseMarkdown` and `messageUseStyler` were removed entirely: messages now use markdown and underlined links by default, and if you want your own attributes you use the `Message` initialiser that takes an `AttributedString`. The README gives the reason, which is that storing an `AttributedString` directly is more efficient than storing a string and applying attributes on the fly.

The fifth change is the one above: the closure arguments of `messageBuilder` and `inputViewBuilder` are now each a single struct, and the two structs are different. The README points at `Sources/ExyteChat/Views/ChatBuilderParameters.swift` for the definitions, which is where you go when you are porting an existing custom cell.

## Two example apps, and where Swift Package Index matters

The repository root is small and tells you what is shipped. Alongside `Sources/` and `Tests/` there is `Package.swift`, `CONTRIBUTING.md`, `LICENSE`, `.swift-format` for the formatter configuration, `.swiftpm/` and `.editorconfig`.

Two example applications are present in the root: `ChatExample/` and `ChatFirestoreExample/`. The second name is the most informative thing about the library's integration story, because it implies a Cloud Firestore backend example rather than a REST or WebSocket one. For anyone evaluating whether the closure-based send path fits their stack, an example wired to a specific backend is worth more than the README.

Distribution is through Swift Package Manager, and the README carries an SPM Compatible badge plus badges served from Swift Package Index for supported Swift versions and platforms. Those badges are the only stated platform information in the README; the prose does not list minimum deployment targets or a Swift version. Check the index rather than assuming, because a chat view with a media picker and a Giphy keyboard pulls in system frameworks whose availability sets the floor.

CocoaPods is still published but on a countdown. The badge reads Cocoapods-Deprecated after 2.4.2, with a link to the pod on cocoapods.org. The badge indicates that the CocoaPods distribution is deprecated after version 2.4.2.

Version state is the open question. The recent releases list a single tag, 2.1.4 from 2025-01-23, while the README documents a migration to version 3. So the documentation is ahead of the tagged release history visible here, and the repository was pushed to on 2026-10-01. Anyone starting fresh should read the version 3 API and check which tag actually carries it before writing code against the older examples.

## Conclusion

Consider ExyteChat if you already have a messaging backend and view model and need an interface layer; the send path is a closure you implement, and you can map `Message` in both directions. The README describes a UI layer rather than a complete chat stack, and it says the Giphy sticker keyboard integrates with Giphy. Verify three things first: that your app's platform and Swift version match what Swift Package Index reports for the package, since the README carries platform and Swift version badges rather than stating them; that you read the Migration to version 3 notes, because five API changes land in one release including the removal of `messageUseMarkdown` and `messageUseStyler` and a change of the `messageBuilder` and `inputViewBuilder` closures to single-struct arguments; and which release carries the documented version 3 API, because the only listed release is 2.1.4 from 2025-01-23 while the README includes migration notes for version 3. The last push was on 2026-10-01 and the license is MIT.

## FAQ

### How does ExyteChat send messages?

It does not. The send button calls the trailing closure you supply, which receives a DraftMessage, and the README states that sending means calling a closure since the user provides the actual API calls. You map Message both ways to your own model or use it as is.

### What content can a message contain in ExyteChat?

Any combination of arbitrarily styled text with AttributedString or markdown, photo and video, audio recording, a link with preview, gif or sticker, documents of any file type picked via the system document picker, location including a live share of 15 minutes, 1 hour or 8 hours, and a custom dictionary of any Sendable.

### What changed in ExyteChat version 3?

Five API changes: enableLoadMore(pageSize...) became enableLoadMore(offset...) with an argument-less trailing closure, linkPreviewsDisabled was refactored into linkPreviewsEnabled, shouldShowLinkPreview was renamed shouldShowPreviewForLink, messageUseMarkdown and messageUseStyler were removed in favour of default markdown, and the messageBuilder and inputViewBuilder closures now take a single struct each.

### How do I customise the message cell in ExyteChat?

Pass a messageBuilder closure that returns a SwiftUI view, as in `ChatView(messages: viewModel.messages) { draft in ... } messageBuilder: { params in ... }`. The parameter type is defined in Sources/ExyteChat/Views/ChatBuilderParameters.swift.

### How do I install ExyteChat?

It is SPM compatible, so you add it as a Swift Package dependency. CocoaPods is also published but the README's badge marks it deprecated after 2.4.2. Supported Swift versions and platforms are shown as Swift Package Index badges in the README.

### What license is ExyteChat released under?

MIT, with the LICENSE file at the repository root and a matching MIT badge in the README.

## Sources

- [exyte/Chat on GitHub](https://github.com/exyte/Chat)
- [Issues](https://github.com/exyte/Chat/issues)
- [License: MIT](https://github.com/exyte/Chat/blob/main/LICENSE)
- [README](https://github.com/exyte/Chat/blob/main/README.md)
- [Releases](https://github.com/exyte/Chat/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/exyte-chat
