Customize Message Composer

The ComposerVC is the component responsible to create new messages or change existing messages. The composer belongs to the ChatMessageListVC component that integrates the message list and the composer in one view. But if you are not using our ChatMessageListVC you can use the composer component in isolation with your own chat view as well. The ComposerVC manages all the logic and actions of the ComposerView, where the latter is only responsible for the composer’s layout.

How to Change the Composer Layout

Since the ComposerView is responsible for the composer’s layout, if you want to change the styling, the position of the views, remove views or add new ones, you need to override the ComposerView. Let’s go through an example of customizing the composer to look like the iMessage app. The iMessage’s composer is quite different than the composer that comes with the SDK: there is only one button to add pictures/videos and the send button is inside the input text container.

BeforeAfter
Composer Imessage Ui Before
Composer Imessage Ui After

After changing the layout through the following steps you should have the result of the picture above.

First thing we need to do is to subclass the ComposerView and change its layout. To have a better understand of how the ComposerView’s layout is structured, please check this page.

class iMessageComposerView: ComposerView {
    override func setUpLayout() {
        super.setUpLayout()

        // Move the send button from the trailing container to input container
        trailingContainer.removeArrangedSubview(sendButton)
        inputMessageView.inputTextContainer.addArrangedSubview(sendButton)

        // Remove spacing in leading container for bigger attachment button
        leadingContainer.spacing = 0

        // Make send button inside input container aligned to bottom
        inputMessageView.inputTextContainer.alignment = .bottom

        // Make the attachment button (camera button) bigger
        attachmentButton.widthAnchor.constraint(equalToConstant: 30).isActive = true
        attachmentButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // Adjust the input corner radius to make the input rounder
        inputMessageView.container.layer.cornerRadius = 18
    }
}

To remove the commands button, we disable the commands feature by subclassing ComposerVC and overriding isCommandsEnabled property, which can also be disabled by the Stream’s Dashboard.

class iMessageComposerVC: ComposerVC {

    override var isCommandsEnabled: Bool {
        false
    }
}

Then, we need to replace the default components with our custom ones:

Components.default.messageComposerView = iMessageComposerView.self
Components.default.messageComposerVC = iMessageComposerVC.self

You can find more information on how the components configuration works here.

Finally, we replace the default attachments button icon with a camera icon:

Appearance.default.images.openAttachments = UIImage(systemName: "camera.fill")!.withTintColor(.systemBlue)

You can find more information on how the appearance configuration works here.

How to add a new Composer Action

All the actions triggered by the Composer buttons are wired in the ComposerVC.setUp() lifecycle. When adding new buttons it is possible to add new actions by overriding this function. Let’s pick the iMessage example and add an emoji button to the composer to open an Emoji picker.

First, we need to add the emoji button to the ComposerView:

class iMessageComposerView: ComposerView {

    lazy var emojiButton: UIButton = {
        let button = UIButton()
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setImage(UIImage(systemName: "face.smiling.fill"), for: .normal)
        return button
    }()

    override func setUpLayout() {
        super.setUpLayout()
        ...

        // Add the emoji button to the left side of attachment button
        leadingContainer.insertArrangedSubview(emojiButton, at: 0)
        // Make the emoji button same size as attachment button
        emojiButton.widthAnchor.constraint(equalToConstant: 30).isActive = true
        emojiButton.heightAnchor.constraint(equalToConstant: 30).isActive = true
    }
BeforeAfter
Composer Imessage Ui After
Composer Imessage Emoji

Now, that we have our emoji button in the composer view, we need to setup an action for it and handle the logic of the new emoji picker. For this, we need to subclass the ComposerVC and override the setUp() function to add a new action and override updateContent() to hide the emoji button when the input text is empty.

class iMessageComposerVC: ComposerVC {

    // We need to cast the composerView to our new `iMessageComposerView`
    // so that we can have access to the new `emojiButton`.
    var iMessageComposerView: iMessageComposerView {
        composerView as! iMessageComposerView
    }

    override func setUp() {
        super.setUp()

        // Add a target action to the emoji button
        iMessageComposerView
            .emojiButton
            .addTarget(self, action: #selector(showEmojiPicker), for: .touchUpInside)
    }

    @objc func showEmojiPicker(sender: UIButton) {

        // For the sake of making the guide simple,
        // we use an alert controller to select emojis.
        let sheetAlertController = UIAlertController(
            title: "Emoji Picker",
            message: nil,
            preferredStyle: .actionSheet
        )

        ["😃", "😇", "😅", "😂"].forEach { emoji in

            let action = UIAlertAction(title: emoji, style: .default) { _ in
                let inputTextView = self.composerView.inputMessageView.textView
                // Populate the emoji in the input view where is the caret position
                inputTextView.replaceSelectedText(emoji)
            }

            sheetAlertController.addAction(action)
        }

        present(sheetAlertController, animated: true)
    }

    override func updateContent() {
        super.updateContent()

        // Hide the emojiButton when the input text is not empty (Input expands)
        self.iMessageComposerView.emojiButton.isHidden = !self.content.text.isEmpty
    }
}

Finally, replace the custom component:

Components.default.messageComposerVC = iMessageComposerVC.self
Components.default.messageComposerView = iMessageComposerView.self

After customizing the ComposerVC with the changes above you should now have an emoji picker when tapping the emoji button. To make the guide simple, we used an UIAlertController for the emoji picker but you can create your own custom emoji picker.

Customizing Typing Suggestions

When typing on the composer, if you type @ it will present a view suggesting users that you can mention, or if you type / as the first character, it will show command suggestions. You can customize not only the UI of the suggestions but also the logic around it, as well as disable this feature.

Disabling the Suggestions

Your app might not want to support suggestions at all like the iMessage app, or want to support only one of the suggestions available. You can disable the suggestions feature by just returning nil on the following functions:

class iMessageComposerVC: ComposerVC {

    override func typingMention(in textView: UITextView) -> (String, NSRange)? {
        return nil
    }

    override func typingCommand(in textView: UITextView) -> String? {
        return nil
    }
}

Changing the Suggestions UI

The UI SDK provides a ChatSuggestionsVC component responsible for showing user suggestions and commands suggestions.

Customizing the Suggestions Container

In case you only want to change the styling of the container that shows both suggestions, you can override the ChatSuggestionsVC. Here is an example of a basic customization of the container component:

class CustomChatSuggestionsVC: ChatSuggestionsVC {
    override func setUpAppearance() {
        super.setUpAppearance()

        collectionView.layer.borderWidth = 1
        collectionView.layer.borderColor = appearance.colorPalette.border.cgColor
        view.layer.shadowOpacity = 0.0
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        collectionView.layer.cornerRadius = 10
    }
}

Then, replace the custom component:

Components.default.suggestionsVC = CustomChatSuggestionsVC.self

Here we simply remove the shadow and customize the border style of the suggestions component.

BeforeAfter
Composer SuggestionsVC Before
Composer SuggestionsVC After

Customizing the Suggestions Cells

If you want to further customize the suggestions UI and want to change the suggestions cells, the recommended way is to subclass the ChatMentionSuggestionView and ChatCommandSuggestionView components. Let’s see an example of how we can change the user mention cell by removing the username and the ”@” symbol:

class CustomMentionSuggestionView: ChatMentionSuggestionView {

    override func setUpLayout() {
        super.setUpLayout()

        textContainer.removeArrangedSubview(usernameTagLabel)
        mentionSymbolImageView.removeFromSuperview()
    }
}

Then, replace the custom component:

Components.default.suggestionsMentionView = CustomMentionSuggestionView.self
BeforeAfter
Composer SuggestionsVC After
Composer SuggestionsVC Custom Cell

Bypassing how the Suggestions are Presented

Finally, if you need to have complete control over how the suggestions are presented and not use our ChatSuggestionsVC component, you can do that by overriding the following functions:

class CustomComposerVC: ComposerVC {
    override func showMentionSuggestions(for typingMention: String, mentionRange: NSRange) {
        // Add your custom implementation to show mention suggestions
    }

    override func showCommandSuggestions(for typingCommand: String) {
        // Add your custom implementation to show command suggestions
    }
}
© Getstream.io, Inc. All Rights Reserved.