Chat | Media | Audio Messages | Extra |
---|---|---|---|
- 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
- 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:
- Text with/without markdown
- Photo/video
- Audio recording Coming soon:
- User's location
- Documents
- Link with preview
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.
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 bottomcomments
- 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 bodyanswer
- when replying to message A, new message with appear direclty 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)
}
You may customize message cells like this:
ChatView(messages: viewModel.messages) { draft in
viewModel.send(draft: draft)
} messageBuilder: { message, positionInUserGroup, 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 userpositionInCommentsGroup
- position of message in its continuous group of comments (only works for .answer ReplyMode, nil for .quote mode)showContextMenuClosure
- closure to show message context menumessageActionClosure
- closure to pass user interaction, .reply for exampleshowAttachmentClosure
- 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 TextFieldattachments
is a struct containing photos, videos, recordings and a message you are replying toinputViewState
- the state of the input view that is controlled by the library automatically if possible or through your calls ofinputViewActionClosure
inputViewStyle
-.message
or.signature
(the chat screen or the photo selection screen)inputViewActionClosure
for calling on taps on your custom buttons. For example, callinputViewActionClosure(.send)
if you want to send your message with your own button, then the library will reset the text and attachments and call thedidSendMessage
sending closuredismissKeyboardClosure
- call this to dismiss keyboard
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 callbcak 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")
}
}
}
ChatView(messages: viewModel.messages) { draft in
viewModel.send(draft: draft)
} messageMenuAction: { (action: Action, defaultActionClosure, message) in // <-- here: specify the name of your `MessageMenuAction` enum
switch action {
case .reply:
defaultActionClosure(message, .reply)
case .edit:
defaultActionClosure(message, .edit { editedText in
// update this message's text on your BE
print(editedText)
})
}
}
messageMenuAction
's parameters:
selectedMenuAction
- action selected by the user from the menu. NOTE: when declaring this variable, specify its type (your custom descendant of MessageMenuAction) explicitlydefaultActionClosure
- a closure taking a case of default implementation of MessageMenuAction which provides simple actions handlers; you call this closure passing the selected message and choosing one of the default actions (.reply, .edit) if you need them; or you can write a custom implementation for all your actions, in that case just ignore this closuremessage
- message for which the menu is displayed
When implementing your own MessageMenuActionClosure
, write a switch statement passing through all the cases of your MessageMenuAction
, inside each case write your own action handler, or call the default one. NOTE: not all default actions work out of the box - e.g. for .edit
you'll still need to provide a closure to save the edited text on your BE. Please see CommentsExampleView in ChatExample project for MessageMenuActionClosure usage example.
These use AnyView
, so please try to keep them easy enough
betweenListAndInputViewBuilder
- content to display in between the chat list view and the input viewmainHeaderBuilder
- a header for the whole chat, which will scroll together with all the messages and headersheaderBuilder
- date section header builder
isListAboveInputView
- messages table above the input field view or not
showDateHeaders
- show section headers with dates between days, default is true
isScrollEnabled
- forbid scrolling for messages' UITabelView
showMessageMenuOnLongPress
- turn menu on long tap on/off
showNetworkConnectionProblem
- display network error on/off
assetsPickerLimit
- set a limit for MediaPicker built into the library
setMediaPickerSelectionParameters
- a struct holding MediaPicker selection parameters (assetsPickerLimit and others like mediaType, selectionStyle, etc.).
orientationHandler
- handle screen rotation
enableLoadMore(offset: Int, handler: @escaping ChatPaginationClosure)
- when user scrolls to offset
-th message from the end, call the handler function, so the user can load more messages
chatNavigation(title: String, status: String? = nil, cover: URL? = nil)
- pass the info for the Chat's navigation bar
avatarSize
- the default avatar is a circle, you can specify its diameter here
tapAvatarClosure
- closure to call on avatar tap
messageUseMarkdown
- use markdown (e.g. ** to make something bold) or not
showMessageTimeView
- show timestamp in a corner of the message
setMessageFont
- pass custom font to use for messages
setAvailableInput
- hide some buttons in default InputView. Available options are:
- .full
- media + text + audio
- .textAndMedia
- .textAndAudio
- .textOnly
There are 2 example projects:
- One has a simple bot posting random text/media messages every 2 seconds. It has no back end and no local storage. Every new start is clean and fresh.
- Another has an integration with Firestore data base. It has all the necessary back end support, including storing media and audio messages, unread messages counters, etc. You'll have to create your own Firestore app and DB. Also replace
GoogleService-Info
with your own. After that you can test on multiple sims/devices.
Create your firestore app https://console.firebase.google.com/ Create firesote database (for light weight text data) https://firebase.google.com/docs/firestore/manage-data/add-data Create cloud firestore database (for images and voice recordings) https://firebase.google.com/docs/storage/web/start
To try out the Chat examples:
- Clone the repo
git clone [email protected]:exyte/Chat.git
- Open terminal and run
cd <ChatRepo>/Example
- Wait for SPM to finish downloading packages
- Run it!
dependencies: [
.package(url: "https://github.com/exyte/Chat.git")
]
pod 'ExyteChat'
github "Exyte/Chat"
- iOS 16+
- Xcode 14+
PopupView - Toasts and popups library
Grid - The most powerful Grid container
ScalingHeaderScrollView - A scroll view with a sticky header which shrinks as you scroll
AnimatedTabBar - A tabbar with number of preset animations
MediaPicker - Customizable media picker
ConcentricOnboarding - Animated onboarding flow
FloatingButton - Floating button menu
ActivityIndicatorView - A number of animated loading indicators
ProgressIndicatorView - A number of animated progress indicators
SVGView - SVG parser
LiquidSwipe - Liquid navigation animation