SkillAgentSearch skills...

Chat

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

Install / Use

/learn @exyte/Chat
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<a href="https://exyte.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/exyte/media/master/common/header-dark.png"><img src="https://raw.githubusercontent.com/exyte/media/master/common/header-light.png"></picture></a>

<a href="https://exyte.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/exyte/media/master/common/our-site-dark.png" width="80" height="16"><img src="https://raw.githubusercontent.com/exyte/media/master/common/our-site-light.png" width="80" height="16"></picture></a>     <a href="https://twitter.com/exyteHQ"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/exyte/media/master/common/twitter-dark.png" width="74" height="16"><img src="https://raw.githubusercontent.com/exyte/media/master/common/twitter-light.png" width="74" height="16"> </picture></a> <a href="https://exyte.com/contacts"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/exyte/media/master/common/get-in-touch-dark.png" width="128" height="24" align="right"><img src="https://raw.githubusercontent.com/exyte/media/master/common/get-in-touch-light.png" width="128" height="24" align="right"></picture></a>

<table> <thead> <tr> <th>Chat</th> <th>Media</th> <th>Audio Messages</th> <th>Extra</th> </tr> </thead> <tbody> <tr> <td> <img src="https://github.com/exyte/Chat/assets/1358172/baf0167f-b3e0-4df2-bd3b-b6b1c4ee385d" /> </td> <td> <img src="https://github.com/exyte/Chat/assets/1358172/d62876ef-4475-4f07-933a-9d9366b02e28" /> </td> <td> <img src="https://github.com/exyte/Chat/assets/1358172/ebd2040d-1cf0-4066-9391-592af1426571" /> </td> <td> <img src="https://github.com/exyte/Chat/assets/1358172/053bcd73-0db7-44da-abd6-0a57f0f88a4b" /> </td> </tr> </tbody> </table> <p><h1>Chat</h1></p> <p><h4>A SwiftUI Chat UI framework with fully customizable message cells and a built-in media picker</h4></p>

SPM Cocoapods License: MIT

Features

  • Displays your messages with pagination and allows you to create and "send" new messages (sending means calling a closure since user will be the one providing actual API calls)
  • Allows you to pass a custom view builder for messages and input views
  • Has a built-in photo and video library/camera picker for multiple media asset selection
  • Sticker keyboard that integrates with Giphy
  • Can display a fullscreen menu on long press a message cell (automatically shows scroll for big messages)
  • Supports "reply to message" via message menu or through a closure. Remove and edit are coming soon
  • This library allows to send the following content in messages in any combination:
    • Arbitrarily styled text with AttributedString or markdown
    • Photo/video
    • Audio recording
    • Link with preview
    • Gif/Sticker Coming soon:
    • User's location
    • Documents

Usage

Create a chat view like this:

@State var messages: [Message] = []

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

where:
messages - list of messages to display
didSendMessage - a closure which is called when the user presses the send button

Message is a type that Chat uses for the internal implementation. In the code above it expects the user to provide a list of Message structs, and it returns a DraftMessage in the didSendMessage closure. You can map it both ways to your own Message model that your API expects or use as is.

Available chat types

Chat type - determines the order of messages and direction of new message animation. Available options:

  • conversation - the latest message is at the bottom, new messages appear from the bottom
  • comments - the latest message is at the top, new messages appear from the top

Reply mode - determines how replying to message looks. Available options:

  • quote - when replying to message A, new message will appear as the newest message, quoting message A in its body
  • answer - when replying to message A, new message with appear directly below message A as a separate cell without duplicating message A in its body

To specify any of these pass them through init:

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

Custom UI

You may customize message cells like this:

ChatView(messages: viewModel.messages) { draft in
    viewModel.send(draft: draft)
} messageBuilder: { message, positionInUserGroup, positionInMessagesSection, positionInCommentsGroup, showContextMenuClosure, messageActionClosure, showAttachmentClosure in
    VStack {
        Text(message.text)
        if !message.attachments.isEmpty {
            ForEach(message.attachments, id: \.id) { at in
                AsyncImage(url: at.thumbnail)
            }
        }
    }
}

messageBuilder's parameters:

  • message - the message containing user info, attachments, etc.
  • positionInUserGroup - the position of the message in its continuous collection of messages from the same user
  • positionInMessagesSection position of message in the section of messages from that day
  • positionInCommentsGroup - position of message in its continuous group of comments (only works for .answer ReplyMode, nil for .quote mode)
  • showContextMenuClosure - closure to show message context menu
  • messageActionClosure - closure to pass user interaction, .reply for example
  • showAttachmentClosure - you can pass an attachment to this closure to use ChatView's fullscreen media viewer

You may customize the input view (a text field with buttons at the bottom) like this:

ChatView(messages: viewModel.messages) { draft in
    viewModel.send(draft: draft)
} inputViewBuilder: { textBinding, attachments, inputViewState, inputViewStyle, inputViewActionClosure, dismissKeyboardClosure in
    Group {
        switch inputViewStyle {
        case .message: // input view on chat screen
            VStack {
                HStack {
                    Button("Send") { inputViewActionClosure(.send) }
                    Button("Attach") { inputViewActionClosure(.photo) }
                }
                TextField("Write your message", text: textBinding)
            }
        case .signature: // input view on photo selection screen
            VStack {
                HStack {
                    Button("Send") { inputViewActionClosure(.send) }
                }
                TextField("Compose a signature for photo", text: textBinding)
                    .background(Color.green)
            }
        }
    }
}

inputViewBuilder's parameters:

  • textBinding to bind your own TextField
  • attachments is a struct containing photos, videos, recordings and a message you are replying to
  • inputViewState - the state of the input view that is controlled by the library automatically if possible or through your calls of inputViewActionClosure
  • inputViewStyle - .message or .signature (the chat screen or the photo selection screen)
  • inputViewActionClosure for calling on taps on your custom buttons. For example, call inputViewActionClosure(.send) if you want to send your message with your own button, then the library will reset the text and attachments and call the didSendMessage sending closure
  • dismissKeyboardClosure - call this to dismiss keyboard

Custom message menu

Long tap on a message will display a menu for this message (can be turned off, see Modifiers). To define custom message menu actions declare an enum conforming to MessageMenuAction. Then the library will show your custom menu options on long tap on message instead of default ones, if you pass your enum's name to it (see code sample). Once the action is selected special callback will be called. Here is a simple example:

enum Action: MessageMenuAction {
    case reply, edit

    func title() -> String {
        switch self {
        case .reply:
            "Reply"
        case .edit:
            "Edit"
        }
    }
    
    func icon() -> Image {
        switch self {
        case .reply:
            Image(systemName: "arrowshape.turn.up.left")
        case .edit:
            Image(systemName: "square.and.pencil")
        }
    }
    
    // Optional
    // Implement this method to conditionally include menu actions on a per message basis
    // The default behavior is to include all menu action items
    static func menuItems(for message: ExyteChat.Message) -> [Action] {
        if message.user.isCurrentUser  {
            return [.edit]
        } else {
            return [.reply]
        }
    }
}

ChatView(messages: viewModel.messages) { draft in
    viewModel.send(draft: draft)
} messageMenuAction: { (action: Action, defaultActionClosure, message) in // <-- here: specify t

Related Skills

View on GitHub
GitHub Stars1.7k
CategoryDevelopment
Updated2h ago
Forks305

Languages

Swift

Security Score

100/100

Audited on Apr 3, 2026

No findings