As evident in the screenshot, there is a large space between the keyboard and the input bar.
What I expect to happen is for the input bar to be flush with the keyboard.
I have tried to follow the example code for MessageKit for MessageContainerController
as closely as possible.
I have already tried disabling IQKeyboardManager altogether.
One thing to note here is that I am pushing this view controller whose navigation controller is a UITabBarController and by setting the view controller’s hideBottomBarWhenPushed
property value to false
so that it covers the whole screen
I have isolated this issue to the container view controller surrounding the main chat view controller.
Also, this code, which is marked as required in the example code for MessageKit
override var inputAccessoryView: UIView? {
chatVC.inputAccessoryView
}
makes the app unresponsive for about a minute then crash
Here’s all the relevant code:
- ChatContainerViewController
import UIKit
import MessageKit
import InputBarAccessoryView
final class ChatContainerController: UIViewController {
let headerView = ChatHeaderView()
let navBar = NavBar(title: "")
let unsafeAreaView: UIView = {
let view = UIView()
view.backgroundColor = .primary
return view
}()
let noDataView = NoDataView(title: "No messages here yet...", body: "Send your first message")
let chatVC = TestChatVC()
override var canBecomeFirstResponder: Bool {
chatVC.canBecomeFirstResponder
}
//
// override var inputAccessoryView: UIView? {
// chatVC.inputAccessoryView
// }
weak var delegate: ChatDelegate?
let api = API()
let senderID: String
var refreshMessagesTimer: Timer? = nil
var justLoaded = true
var userPhoneNumber = String()
init(title: String, senderID: String) {
self.senderID = senderID
super.init(nibName: nil, bundle: nil)
navigationController?.setNavigationBarHidden(false, animated: true)
navBar.titleLabel.text = title
setupLayout()
setupCallbacks()
setupTimer()
getChatsSync(senderID: senderID)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
}
func getChatsSync(senderID: String) {
if justLoaded {
justLoaded = false
Task {
LoadingIndicator.show()
await getChats(senderID: senderID)
LoadingIndicator.hide()
}
} else {
Task { await getChats(senderID: senderID) }
}
}
func setupTimer(){
self.refreshMessagesTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [weak self] timer in
guard let self else { return }
self.getChatsSync(senderID: senderID)
})
}
func getChats(senderID: String) async {
do {
let chatData = try await api.getChats(senderID: senderID)
let chats = chatData?.conversations
let users = chatData?.users
let receiver = users?.first(where: {String($0.id ?? 0) == UserData.userID})
let sender = users?.first(where: {String($0.id ?? 0) != UserData.userID})
setupHeaderView(for: sender)
chatVC.receiver = Sender(senderId: String(sender?.id ?? 0), displayName: UserData.username, avatarURL: receiver?.avatarURL ?? "")
chatVC.sender = Sender(senderId: String(receiver?.id ?? 0), displayName: sender?.firstName ?? "", avatarURL: sender?.avatarURL ?? "")
let mappedMessages: [Message] = chats?.compactMap { conversation in
let sender = (conversation.senderID == Int(UserData.userID) ?? 0) ? chatVC.receiver : chatVC.sender
let messageId = String(conversation.id ?? 0)
let sentDate = iso8601formattedDate(date: conversation.createdAt)
let messageText = conversation.message ?? ""
return Message(sender: sender, messageId: messageId, sentDate: sentDate, kind: .text(messageText))
}.reversed() ?? []
chatVC.messages = mappedMessages
noDataView.isHidden = (!(chats?.isEmpty ?? false))
} catch {
// Handle error
}
}
func sendMessage(receiverID: String, message: String) async -> Bool {
do {
let isSent = try await api.sendMessage(receiverID: receiverID, message: message)
if isSent {
getChatsSync(senderID: senderID)
}
return isSent
} catch {
return true
}
}
func setupHeaderView(for user: User?){
guard let user else { print("user is nil"); return }
headerView.nameLabel.text = user.firstName
headerView.emailAddressLabel.text = user.email
headerView.profileImage.setImageWith(user.avatarURL)
userPhoneNumber = user.mobile.orEmptyString
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
refreshMessagesTimer?.invalidate()
}
fileprivate func setupCallbacks(){
headerView.callButton.addTarget(self, action: #selector(callButtonPressed), for: .touchUpInside)
}
@objc
func callButtonPressed(){
dialPhoneNumber(userPhoneNumber)
}
private func setupLayout() {
chatVC.delegate = self
chatVC.willMove(toParent: self)
addChild(chatVC)
view.addSubview(chatVC.view)
chatVC.didMove(toParent: self)
view.addSubview(headerView)
view.addSubview(navBar)
view.addSubview(noDataView)
view.addSubview(unsafeAreaView)
unsafeAreaView.translatesAutoresizingMaskIntoConstraints = false
navBar.translatesAutoresizingMaskIntoConstraints = false
chatVC.view.translatesAutoresizingMaskIntoConstraints = false
headerView.translatesAutoresizingMaskIntoConstraints = false
noDataView.translatesAutoresizingMaskIntoConstraints = false
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
NSLayoutConstraint.activate([
chatVC.view.topAnchor.constraint(equalTo: headerView.bottomAnchor),
chatVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
chatVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
chatVC.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
unsafeAreaView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
unsafeAreaView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
unsafeAreaView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
unsafeAreaView.heightAnchor.constraint(equalToConstant: 5000),
navBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
navBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
navBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
headerView.topAnchor.constraint(equalTo: navBar.bottomAnchor),
headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
noDataView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
noDataView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
noDataView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
noDataView.bottomAnchor.constraint(equalTo: chatVC.messageInputBar.topAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension ChatContainerController: ChatDelegate {
func sendMessage(message: String, receiverID: String, completed: @escaping ((Bool) -> Void)) {
Task {
let isSent = await sendMessage(receiverID: receiverID, message: message)
completed(isSent)
}
}
}
- ChatViewController
import Foundation
import UIKit
import Stevia
import SDWebImage
import MessageKit
import InputBarAccessoryView
protocol ChatDelegate: AnyObject {
func sendMessage(message: String, receiverID: String, completed: @escaping ((Bool) -> Void))
}
class ChatVC: MessagesViewController, MessagesDataSource, InputBarAccessoryViewDelegate {
weak var delegate: ChatDelegate?
var receiver = Sender(senderId: "0", displayName: "Maaz", avatarURL: "")
var sender = Sender(senderId: "0", displayName: "Hussain", avatarURL: "")
var messages: [MessageType] = [] {
didSet {
if messages.isEmpty {
messagesCollectionView.reloadData()
messagesCollectionView.scrollToLastItem(animated: true)
} else {
messagesCollectionView.reloadDataAndKeepOffset()
messagesCollectionView.scrollToLastItem(animated: true)
}
}
}
override func viewDidLoad() {
configureMessageCollectionView()
configureMessageInputBar()
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.becomeFirstResponder()
}
func configureMessageCollectionView() {
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messageCellDelegate = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
scrollsToLastItemOnKeyboardBeginsEditing = true
maintainPositionOnInputBarHeightChanged = true
showMessageTimestampOnSwipeLeft = true
}
func configureMessageInputBar() {
messageInputBar.delegate = self
messageInputBar.inputTextView.tintColor = .primary
messageInputBar.sendButton.setTitleColor(.primary, for: .normal)
messageInputBar.sendButton.setTitleColor(.primary.withAlphaComponent(0.3), for: .highlighted)
}
func customizeNavigationBar() {
navigationController?.navigationBar.tintColor = UIColor.black
navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: UIFont.CustomFont(.bold, size: 19)]
let backButtonImage: UIImage = isArabic ? .leftArrow.withHorizontallyFlippedOrientation() : .leftArrow
let backButton = UIBarButtonItem(image: backButtonImage, style: .plain, target: self, action: #selector(backButtonPressed))
navigationItem.leftBarButtonItem = backButton
navigationController?.navigationBar.barTintColor = UIColor.black
navigationController?.navigationBar.backgroundColor = .primary
let unsafeAreaView = UIView()
unsafeAreaView.backgroundColor = .primary
view.subviews { unsafeAreaView }
NSLayoutConstraint.activate([
unsafeAreaView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
unsafeAreaView.heightAnchor.constraint(equalToConstant: 500),
unsafeAreaView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
unsafeAreaView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
@objc
func backButtonPressed(){
self.navigationController?.popViewController(animated: true)
}
}
extension ChatVC: MessagesLayoutDelegate, MessagesDisplayDelegate, MessageCellDelegate {
//MARK: Message Data Source
var currentSender: MessageKit.SenderType {
return receiver
}
func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessageKit.MessagesCollectionView) -> MessageKit.MessageType {
return messages[indexPath.section]
}
func numberOfSections(in messagesCollectionView: MessageKit.MessagesCollectionView) -> Int {
print(messages.count)
return messages.count
}
//MARK: Message styling
func messageStyle(for message: MessageType, at index: IndexPath, in _: MessagesCollectionView) -> MessageStyle {
let tail: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft
if isNextMessageSameSender(at: index){
return .bubble
} else {
return .bubbleTail(tail, .pointedEdge)
}
}
func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) {
if let messageSender = messages[indexPath.section].sender as? Sender {
let displayName = messageSender.initials
let imageURL = URL(string: messageSender.avatarURL)
SDWebImageManager.shared.loadImage(with: imageURL, progress: nil) { image, _, _, _, _, _ in
let avatar = Avatar(image: image, initials: displayName)
avatarView.set(avatar: avatar)
avatarView.isHidden = self.isNextMessageSameSender(at: indexPath)
}
}
}
//MARK: Helpers
func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath)
}
func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool {
guard indexPath.section - 1 >= 0 else { return false }
return messages[indexPath.section].sender.senderId == messages[indexPath.section - 1].sender.senderId
}
func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
guard indexPath.section + 1 < messages.count else { return false }
return messages[indexPath.section].sender.senderId == messages[indexPath.section + 1].sender.senderId
}
}
extension ChatVC {
@objc
func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
processInputBar(inputBar)
}
func processInputBar(_ inputBar: InputBarAccessoryView){
inputBar.inputTextView.resignFirstResponder()
let message = inputBar.inputTextView.text
inputBar.inputTextView.text = String()
inputBar.invalidatePlugins()
inputBar.sendButton.startAnimating()
inputBar.inputTextView.placeholder = "Sending...".localized()
delegate?.sendMessage(message: message.orEmptyString, receiverID: receiver.senderId, completed: { completed in
guard completed else { return }
inputBar.sendButton.stopAnimating()
inputBar.inputTextView.placeholder = "Aa".localized()
})
}
}
struct Sender: SenderType {
var senderId: String
var displayName: String
var initials: String {
let nameComponents = displayName.split(separator: " ")
guard let firstNameInitial = nameComponents.first?.first else { return "" }
return String(firstNameInitial).uppercased() + (nameComponents.count > 1 ? String(nameComponents.last!.first!).uppercased() : "")
}
var avatarURL: String
}
struct Message: MessageType {
var sender: MessageKit.SenderType
var messageId: String
var sentDate: Date
var kind: MessageKit.MessageKind
}