UIViewControllerRepresentable MessageKit UICollectionView scrolls past bottom item when keyboard is shown

739 views Asked by At

I am building an app with SwiftUI and I wanted to use MessageKit, so I used a UIViewControllerRepresentable to show a MessageKit view. However, when the keyboard is shown messages are able to scroll past the bottom of the view.

Here is a screenshot of the problem: scroll past bottom with keyboard shown

I can still scroll the messages back down, but trying to push the UICollectionView to the bottom results in the same problem. This problem does not occur when the keyboard is hidden.

Here is a screenshot of the captured UI hierarchy with the navigation bar and root views hidden: view hierarchy

The collection view is still sized correctly but messages are able to scroll past the bottom and the scroll indicators are way up at the top.

Here is the root SwiftUI view with relevant code:

struct SingleChatView: SwiftUI.View {
    @ObservedObject var chat: Chat
    
    @State private var mkView: MessageKitView
    
    init(chat: Chat) {
        self.chat = chat
        _mkView = State(initialValue: MessageKitView(chat: chat))
    }
    
    var body: some SwiftUI.View {
        mkView
            .navigationBarTitle(chat.name)
            .uiKitOnAppear {
                mkView.controller.becomeFirstResponder()
                mkView.controller.messagesCollectionView.scrollToBottom(animated: true)
                
                chat.clearNotifications()
                
                // Set chat as active
                ChatManager.shared.activeChat = chat
            }
            .onDisappear {
                chat.setTypingStatus(to: false)
                // Set chat as inactive
                ChatManager.shared.activeChat = nil
            }
    }
}

// Different file
struct UIKitAppear: UIViewControllerRepresentable {
    let action: () -> Void
    func makeUIViewController(context: Context) -> UIAppearViewController {
       let vc = UIAppearViewController()
        vc.action = action
        return vc
    }
    func updateUIViewController(_ controller: UIAppearViewController, context: Context) {
    }
}
final class UIAppearViewController: UIViewController {
    var action: () -> Void = {}
    override func viewDidLoad() {
        view.addSubview(UILabel())
    }
    override func viewDidAppear(_ animated: Bool) {
        action()
    }
}
public extension View {
    func uiKitOnAppear(_ perform: @escaping () -> Void) -> some View {
        self.background(UIKitAppear(action: perform))
    }
}

Here is the root UIKit view controller:

struct MessageKitView: UIViewControllerRepresentable {
    @ObservedObject var chat: Chat
    @EnvironmentObject var userData: UserData
    
    @State var controller = ChatViewController()
    @State var refreshControl = UIRefreshControl()
    
    init(chat: Chat) {
        self.chat = chat
    }
    
    func makeUIViewController(context: Context) -> MessagesViewController {
        return controller
    }
    
    func updateUIViewController(_ uiViewController: MessagesViewController, context: Context) {
        uiViewController.messagesCollectionView.messagesDataSource = context.coordinator
        uiViewController.messagesCollectionView.messagesLayoutDelegate = context.coordinator
        uiViewController.messagesCollectionView.messagesDisplayDelegate = context.coordinator
        uiViewController.messagesCollectionView.messageCellDelegate = context.coordinator
        
        uiViewController.showMessageTimestampOnSwipeLeft = true
        
        uiViewController.setTypingIndicatorViewHidden(chat.typers.isEmpty, animated: true)
        
        // Refresh Control
        refreshControl.addTarget(context.coordinator, action: #selector(context.coordinator.onRefresh), for: .valueChanged)
        uiViewController.messagesCollectionView.refreshControl = refreshControl
        
        uiViewController.messageInputBar.delegate = context.coordinator
        uiViewController.messageInputBar.inputTextView.isImagePasteEnabled = false
        //uiViewController.maintainPositionOnKeyboardFrameChanged = true
        uiViewController.scrollsToBottomOnKeyboardBeginsEditing = true
        //uiViewController.scrollsToLastItemOnKeyboardBeginsEditing = true
        
        uiViewController.messagesCollectionView.reloadData()
        //uiViewController.messagesCollectionView.scrollToLastItem(at: .bottom, animated: true)
        //uiViewController.messagesCollectionView.scrollToBottom(animated: true)
        
        // Removes avatar from outgoing messages
        if let layout = uiViewController.messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout {
            layout.textMessageSizeCalculator.outgoingAvatarSize = .zero
            layout.textMessageSizeCalculator.outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: .zero)
            layout.emojiMessageSizeCalculator.outgoingAvatarSize = .zero
            layout.emojiMessageSizeCalculator.outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: .zero)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    final class Coordinator {
        let parent: MessageKitView
        
        init(_ parent: MessageKitView) {
            self.parent = parent
        }
        
        @objc func onRefresh() {
            parent.chat.loadMessages(userData: parent.userData, withPromise: FailurePromise(success: {
                self.parent.refreshControl.endRefreshing()
            }, failure: { failure in
                self.parent.refreshControl.endRefreshing()
                print(failure)
            }))
        }
    }
}

The commented out lines represent parameters I have tried to tweak. There are more extensions to Coordinator but I will only include them upon request because I don't think they are relevant to my issue. What could I be doing wrong here and how should I try to fix it?

0

There are 0 answers