open class CustomChannelViewController: UIViewController, ThemeProvider {
/// Controller for observing data changes within the channel.
open var channelController: ChatChannelController!
/// The message list component responsible to render the messages.
open lazy var messageListVC: ChatMessageListVC = ChatMessageListVC()
/// Controller that handles the composer view.
open lazy var messageComposerVC = ComposerVC()
override open func viewDidLoad() {
super.viewDidLoad()
// Setup
messageListVC.delegate = self
messageListVC.dataSource = self
messageListVC.client = ChatClient.shared
// Setup Channel Controller
channelController.delegate = self
channelController.synchronize()
// Layout
messageListVC.view.translatesAutoresizingMaskIntoConstraints = false
addChildViewController(messageListVC, targetView: view)
NSLayoutConstraint.activate([
messageListVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
messageListVC.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
messageListVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
messageListVC.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}
}
Message List
The ChatMessageListVC
is a component that renders a list of messages. It decides how to render a message based on its type and content. The messages data should be provided through the data source named ChatMessageListVCDataSource
and some important actions should be delegated through the ChatMessageListVCDelegate
, very similar to how the native UITableView
and UICollectionView
works.
The Stream SDK already provides a ChatChannelVC
and a ChatThreadVC
that use the ChatMessageListVC
to render the messages from a Channel and Thread, respectively. Both components are a full-featured Chat view since both include the message list to render the messages, and the composer to create new messages.
Usage
If the built-in ChatChannelVC
and ChatThreadVC
components do not suit your need, you can use the ChatMessageListVC
on your custom views.
In order to properly configure the ChatMessageListVC
these are the required dependencies:
client: ChatClient
, the Stream Chat client instance.dataSource: ChatMessageListVCDataSource
, the data source for theChatMessageListVC
. The data source is responsible for providing the messages to be rendered, these messages can be provided by a Channel or a Thread, for example.delegate: ChatMessageListVCDelegate
, the delegate for theChatMessageListVC
. The delegate is responsible for handling the actions that are triggered by the user when interacting with the message list.
To add the ChatMessageListVC
to your view, you need to add it as a child view controller:
In order to be easier to set up child view controllers you can add this extension to your application:
extension UIViewController {
func addChildViewController(_ child: UIViewController, targetView superview: UIView) {
child.willMove(toParent: self)
addChild(child)
superview.addSubview(child.view)
child.didMove(toParent: self)
}
}
For simplicity, the code above doesn’t describe how to set up the message composer, in case you don’t have your own message composer and want to set up the one from Stream, you can read the Message Composer documentation.
After adding the message list as a child view controller and configuring its dependencies we need to implement the ChatMessageListVCDataSource
to connect the messages from the ChannelController
to the ChatMessageListVC
. In this case, we are using a ChannelController
since we are interested in showing the messages of a channel, but a MessageController
could also be used to display the replies of a message thread.
extension ChannelViewController: ChatMessageListVCDataSource {
open func channel(for vc: ChatMessageListVC) -> ChatChannel? {
channelController.channel
}
open func numberOfMessages(in vc: ChatMessageListVC) -> Int {
channelController.messages.count
}
open func chatMessageListVC(_ vc: ChatMessageListVC, messageAt indexPath: IndexPath) -> ChatMessage? {
return channelController.messages[indexPath.item]
}
open func chatMessageListVC(
_ vc: ChatMessageListVC,
messageLayoutOptionsAt indexPath: IndexPath
) -> ChatMessageLayoutOptions {
guard let channel = channelController.channel else { return [] }
return messageListVC.components.messageLayoutOptionsResolver.optionsForMessage(
at: indexPath,
in: channel,
with: AnyRandomAccessCollection(channelController.messages),
appearance: messageListVC.appearance
)
}
}
Next, we need to implement the ChatMessageListVCDelegate
to handle the actions that are triggered by the user when interacting with the message list.
extension ChannelViewController: ChatMessageListVCDelegate {
open func chatMessageListVC(
_ vc: ChatMessageListVC,
willDisplayMessageAt indexPath: IndexPath
) {
// Load previous messages before showing the last 10 messages.
if indexPath.row < channelController.messages.count - 10 {
return
}
channelController.loadPreviousMessages()
}
open func chatMessageListVC(
_ vc: ChatMessageListVC,
didTapOnAction actionItem: ChatMessageActionItem,
for message: ChatMessage
) {
// Handle message actions
switch actionItem {
case is EditActionItem:
dismiss(animated: true) { [weak self] in
self?.messageComposerVC.content.editMessage(message)
}
case is InlineReplyActionItem:
dismiss(animated: true) { [weak self] in
self?.messageComposerVC.content.quoteMessage(message)
}
case is ThreadReplyActionItem:
dismiss(animated: true) { [weak self] in
self?.messageListVC.showThread(messageId: message.id)
}
default:
return
}
}
open func chatMessageListVC(_ vc: ChatMessageListVC, scrollViewDidScroll scrollView: UIScrollView) {
// Handle scroll events, and check if the last message was read, to mark the channel read.
if messageListVC.listView.isLastCellFullyVisible, channelController.channel?.isUnread == true {
channelController.markRead()
}
}
}
Currently, by implementing the ChatMessageListVCDelegate
we are able to handle when a user performs an action on a message, when a message will be displayed, and when the user is scrolling the message list. More events might be added in the future, but for now, these should be enough to implement the most common features in a chat view, like pagination, marking the channel as read when the user scrolls to the bottom, and handling message actions.
Finally, we need to implement the ChannelControllerDelegate
to handle the events from the ChannelController
. This will make sure that the messages are always in sync with the server.
extension ChannelViewController: ChatChannelControllerDelegate {
open func channelController(
_ channelController: ChatChannelController,
didUpdateMessages changes: [ListChange<ChatMessage>]
) {
messageListVC.updateMessages(with: changes)
}
open func channelController(
_ channelController: ChatChannelController,
didUpdateChannel channel: EntityChange<ChatChannel>
) {
let channelUnreadCount = channelController.channel?.unreadCount ?? .noUnread
messageListVC.scrollToLatestMessageButton.content = channelUnreadCount
}
open func channelController(
_ channelController: ChatChannelController,
didChangeTypingUsers typingUsers: Set<ChatUser>
) {
guard channelController.areTypingEventsEnabled else { return }
let currentUserId = channelController.client.currentUserId
let typingUsersWithoutCurrentUser = typingUsers
.sorted { $0.id < $1.id }
.filter { $0.id != currentUserId }
if typingUsersWithoutCurrentUser.isEmpty {
messageListVC.hideTypingIndicator()
} else {
messageListVC.showTypingIndicator(typingUsers: typingUsersWithoutCurrentUser)
}
}
}
UI Customization
You can customize the message list by subclassing the ChatMessageListVC
and replacing the Components.default.messageListVC
component.
Components.default.messageListVC = CustomMessageListVC.self
You can find more information on how the components configuration works here.
Message Content View
In order to change how the messages are rendered, you need to subclass the ChatMessageContentView
and replace it in the Components.default.messageContentView
. For more details on how you can customize the message content view, you can take a look at the Customizing Messages documentation.
You can also set your custom ChatMessageContentView
in the ChatMessageListVC.cellContentClassForMessage()
function, this is especially useful if you have multiple instances of ChatMessageListVC
and each have different ChatMessageContentView
’s.
final class CustomMessageListVC: ChatMessageListVC {
override func cellContentClassForMessage(at indexPath: IndexPath) -> ChatMessageContentView.Type {
CustomChatMessageContentView.self
}
}
As you can see above, by overriding the cellContentClassForMessage(at:)
function we can change the ChatMessageContentView
that is used to render the message.
Message List Layout
Like any other component in the SDK, you can customize the message list layout by overriding the setUpLayout()
lifecycle method when subclassing ChatMessageListVC
.
final class CustomMessageListVC: ChatMessageListVC {
override func setUpLayout() {
super.setUpLayout()
NSLayoutConstraint.activate([
scrollToLatestMessageButton.centerXAnchor.constraint(equalTo: view.layoutMarginsGuide.centerXAnchor)
])
dateOverlayView.removeFromSuperview()
}
}
In the simple example above, we moved the scrollToLatestMessageButton
to the center bottom of the message list, instead of the bottom right corner, and also removed the dateOverlayView
from the view hierarchy.
Navigation
The message list uses the ChatMessageListRouter
navigation component to handle the routing, like for example showing threads and attachment previews, as well as the popup actions view. You can customize the navigation by providing your own.
Components.default.messageListRouter = CustomChatMessageListRouter()
You can find more information on how the components configuration works here.
Properties
dataSource
The object that acts as the data source of the message list.
public weak var dataSource: ChatMessageListVCDataSource?
delegate
The object that acts as the delegate of the message list.
public weak var delegate: ChatMessageListVCDelegate?
client
The root object representing the Stream Chat.
public var client: ChatClient!
router
The router object that handles navigation to other view controllers.
open lazy var router: ChatMessageListRouter
listView
A View used to display the messages
open private(set) lazy var listView: ChatMessageListView
dateOverlayView
A View used to display date of currently displayed messages
open private(set) lazy var dateOverlayView: ChatMessageListScrollOverlayView
typingIndicatorView
A View which displays information about current users who are typing.
open private(set) lazy var typingIndicatorView: TypingIndicatorView = components
.typingIndicatorView
.init()
.withoutAutoresizingMaskConstraints
typingIndicatorViewHeight
The height of the typing indicator view
open private(set) var typingIndicatorViewHeight: CGFloat = 28
isTypingEventsEnabled
A Boolean value indicating whether the typing events are enabled.
open var isTypingEventsEnabled: Bool
scrollToLatestMessageButton
A button to scroll the collection view to the bottom. Visible when there is unread message and the collection view is not at the bottom already.
open private(set) lazy var scrollToLatestMessageButton: ScrollToLatestMessageButton = components
.scrollToLatestMessageButton
.init()
.withoutAutoresizingMaskConstraints
isScrollToBottomButtonVisible
open var isScrollToBottomButtonVisible: Bool
Methods
traitCollectionDidChange(_:)
override open func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
setUp()
override open func setUp()
setUpLayout()
override open func setUpLayout()
setUpAppearance()
override open func setUpAppearance()
cellLayoutOptionsForMessage(at:)
Returns layout options for the message on given indexPath
.
open func cellLayoutOptionsForMessage(at indexPath: IndexPath) -> ChatMessageLayoutOptions
Layout options are used to determine the layout of the message. By default there is one message with all possible layout and layout options determines which parts of the message are visible for the given message.
cellContentClassForMessage(at:)
Returns the content view class for the message at given indexPath
open func cellContentClassForMessage(at indexPath: IndexPath) -> ChatMessageContentView.Type
attachmentViewInjectorClassForMessage(at:)
Returns the attachment view injector for the message at given indexPath
open func attachmentViewInjectorClassForMessage(at indexPath: IndexPath) -> AttachmentViewInjector.Type?
setScrollToLatestMessageButton(visible:animated:)
Set the visibility of scrollToLatestMessageButton
.
open func setScrollToLatestMessageButton(visible: Bool, animated: Bool = true)
scrollToLatestMessage()
Action for scrollToLatestMessageButton
that scroll to most recent message.
@objc open func scrollToLatestMessage()
scrollToMostRecentMessage(animated:)
Scrolls to most recent message
open func scrollToMostRecentMessage(animated: Bool = true)
updateMessages(with:completion:)
Updates the collection view data with given changes
.
open func updateMessages(with changes: [ListChange<ChatMessage>], completion: (() -> Void)? = nil)
handleTap(_:)
Handles tap action on the table view.
@objc open func handleTap(_ gesture: UITapGestureRecognizer)
Default implementation will dismiss the keyboard if it is open
handleLongPress(_:)
Handles long press action on collection view.
@objc open func handleLongPress(_ gesture: UILongPressGestureRecognizer)
Default implementation will convert the gesture location to collection view’s indexPath
and then call selection action on the selected cell.
didSelectMessageCell(at:)
The message cell was select and should show the available message actions.
open func didSelectMessageCell(at indexPath: IndexPath)
Parameters
indexPath
: The index path that the message was selected.
showThread(messageId:)
Opens thread detail for given MessageId
.
open func showThread(messageId: MessageId)
showTypingIndicator(typingUsers:)
Shows typing Indicator.
open func showTypingIndicator(typingUsers: [ChatUser])
Parameters
typingUsers
: typing users gotten fromchannelController
hideTypingIndicator()
Hides typing Indicator.
open func hideTypingIndicator()
numberOfSections(in:)
open func numberOfSections(in tableView: UITableView) -> Int
tableView(_:numberOfRowsInSection:)
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
tableView(_:cellForRowAt:)
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
tableView(_:willDisplay:forRowAt:)
open func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
scrollViewDidScroll(_:)
open func scrollViewDidScroll(_ scrollView: UIScrollView)
scrollOverlay(_:textForItemAt:)
open func scrollOverlay(
_ overlay: ChatMessageListScrollOverlayView,
textForItemAt indexPath: IndexPath
) -> String?
chatMessageActionsVC(_:message:didTapOnActionItem:)
open func chatMessageActionsVC(
_ vc: ChatMessageActionsVC,
message: ChatMessage,
didTapOnActionItem actionItem: ChatMessageActionItem
)
chatMessageActionsVCDidFinish(_:)
open func chatMessageActionsVCDidFinish(_ vc: ChatMessageActionsVC)
messageContentViewDidTapOnErrorIndicator(_:)
open func messageContentViewDidTapOnErrorIndicator(_ indexPath: IndexPath?)
messageContentViewDidTapOnThread(_:)
open func messageContentViewDidTapOnThread(_ indexPath: IndexPath?)
messageContentViewDidTapOnQuotedMessage(_:)
open func messageContentViewDidTapOnQuotedMessage(_ indexPath: IndexPath?)
messageContentViewDidTapOnAvatarView(_:)
open func messageContentViewDidTapOnAvatarView(_ indexPath: IndexPath?)
galleryMessageContentView(at:didTapAttachmentPreview:previews:)
open func galleryMessageContentView(
at indexPath: IndexPath?,
didTapAttachmentPreview attachmentId: AttachmentId,
previews: [GalleryItemPreview]
)
galleryMessageContentView(at:didTakeActionOnUploadingAttachment:)
open func galleryMessageContentView(
at indexPath: IndexPath?,
didTakeActionOnUploadingAttachment attachmentId: AttachmentId
)
didTapOnLinkAttachment(_:at:)
open func didTapOnLinkAttachment(
_ attachment: ChatMessageLinkAttachment,
at indexPath: IndexPath?
)
didTapOnAttachment(_:at:)
open func didTapOnAttachment(
_ attachment: ChatMessageFileAttachment,
at indexPath: IndexPath?
)
didTapOnAttachmentAction(_:at:)
Executes the provided action on the message
open func didTapOnAttachmentAction(
_ action: AttachmentAction,
at indexPath: IndexPath
)
gestureRecognizer(_:shouldReceive:)
open func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch
) -> Bool
presentationControllerShouldDismiss(_:)
public func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool
- Usage
- UI Customization
- Navigation
- Properties
- Methods
- traitCollectionDidChange(_:)
- setUp()
- setUpLayout()
- setUpAppearance()
- cellLayoutOptionsForMessage(at:)
- cellContentClassForMessage(at:)
- attachmentViewInjectorClassForMessage(at:)
- setScrollToLatestMessageButton(visible:animated:)
- scrollToLatestMessage()
- scrollToMostRecentMessage(animated:)
- updateMessages(with:completion:)
- handleTap(_:)
- handleLongPress(_:)
- didSelectMessageCell(at:)
- showThread(messageId:)
- showTypingIndicator(typingUsers:)
- hideTypingIndicator()
- numberOfSections(in:)
- tableView(_:numberOfRowsInSection:)
- tableView(_:cellForRowAt:)
- tableView(_:willDisplay:forRowAt:)
- scrollViewDidScroll(_:)
- scrollOverlay(_:textForItemAt:)
- chatMessageActionsVC(_:message:didTapOnActionItem:)
- chatMessageActionsVCDidFinish(_:)
- messageContentViewDidTapOnErrorIndicator(_:)
- messageContentViewDidTapOnThread(_:)
- messageContentViewDidTapOnQuotedMessage(_:)
- messageContentViewDidTapOnAvatarView(_:)
- galleryMessageContentView(at:didTapAttachmentPreview:previews:)
- galleryMessageContentView(at:didTakeActionOnUploadingAttachment:)
- didTapOnLinkAttachment(_:at:)
- didTapOnAttachment(_:at:)
- didTapOnAttachmentAction(_:at:)
- gestureRecognizer(_:shouldReceive:)
- presentationControllerShouldDismiss(_:)