Files
MessageKit/Sources/Controllers/MessagesViewController+Keyboard.swift
2024-05-27 19:01:04 +02:00

153 lines
5.9 KiB
Swift

// MIT License
//
// Copyright (c) 2017-2022 MessageKit
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import Combine
import Foundation
import InputBarAccessoryView
import UIKit
extension MessagesViewController {
// MARK: Internal
// MARK: - Register Observers
internal func addKeyboardObservers() {
keyboardManager.bind(
inputAccessoryView: inputContainerView,
withAdditionalBottomSpace: { [weak self] in self?.inputBarAdditionalBottomSpace() ?? 0 }
)
keyboardManager.bind(to: messagesCollectionView)
/// Observe didBeginEditing to scroll content to last item if necessary
NotificationCenter.default
.publisher(for: UITextView.textDidBeginEditingNotification)
.subscribe(on: DispatchQueue.global())
/// Wait for inputBar frame change animation to end
.delay(for: .milliseconds(200), scheduler: DispatchQueue.main)
.receive(on: DispatchQueue.main)
.sink { [weak self] notification in
self?.handleTextViewDidBeginEditing(notification)
}
.store(in: &disposeBag)
NotificationCenter.default
.publisher(for: UITextView.textDidChangeNotification)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.compactMap { $0.object as? InputTextView }
.filter { [weak self] textView in
textView == self?.messageInputBar.inputTextView
}
.map(\.text)
.removeDuplicates()
.delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly
.sink { [weak self] _ in
self?.updateMessageCollectionViewBottomInset()
if !(self?.maintainPositionOnInputBarHeightChanged ?? false) {
self?.messagesCollectionView.scrollToLastItem()
}
}
.store(in: &disposeBag)
NotificationCenter.default
.publisher(for: UITextInputMode.currentInputModeDidChangeNotification)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.removeDuplicates()
.delay(for: .milliseconds(50), scheduler: DispatchQueue.main) /// Wait for next runloop to lay out inputView properly
.sink { [weak self] _ in
self?.updateMessageCollectionViewBottomInset()
if !(self?.maintainPositionOnInputBarHeightChanged ?? false) {
self?.messagesCollectionView.scrollToLastItem()
}
}
.store(in: &disposeBag)
/// Observe frame change of the input bar container to update collectioView bottom inset
inputContainerView.publisher(for: \.center)
.receive(on: DispatchQueue.main)
.removeDuplicates()
.sink(receiveValue: { [weak self] _ in
self?.updateMessageCollectionViewBottomInset()
})
.store(in: &disposeBag)
}
// MARK: - Updating insets
/// Updates bottom messagesCollectionView inset based on the position of inputContainerView
internal func updateMessageCollectionViewBottomInset() {
let collectionViewHeight = messagesCollectionView.frame.maxY
let newBottomInset = collectionViewHeight - (inputContainerView.frame.minY - additionalBottomInset) -
automaticallyAddedBottomInset
let normalizedNewBottomInset = max(0, newBottomInset)
let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset
UIView.performWithoutAnimation {
guard differenceOfBottomInset != 0 else { return }
messagesCollectionView.contentInset.bottom = normalizedNewBottomInset
messagesCollectionView.verticalScrollIndicatorInsets.bottom = newBottomInset
}
}
// MARK: Private
/// UIScrollView can automatically add safe area insets to its contentInset,
/// which needs to be accounted for when setting the contentInset based on screen coordinates.
///
/// - Returns: The distance automatically added to contentInset.bottom, if any.
private var automaticallyAddedBottomInset: CGFloat {
messagesCollectionView.adjustedContentInset.bottom - messageCollectionViewBottomInset
}
private var messageCollectionViewBottomInset: CGFloat {
messagesCollectionView.contentInset.bottom
}
/// UIScrollView can automatically add safe area insets to its contentInset,
/// which needs to be accounted for when setting the contentInset based on screen coordinates.
///
/// - Returns: The distance automatically added to contentInset.top, if any.
private var automaticallyAddedTopInset: CGFloat {
messagesCollectionView.adjustedContentInset.top - messageCollectionViewTopInset
}
private var messageCollectionViewTopInset: CGFloat {
messagesCollectionView.contentInset.top
}
// MARK: - Private methods
private func handleTextViewDidBeginEditing(_ notification: Notification) {
guard
scrollsToLastItemOnKeyboardBeginsEditing,
let inputTextView = notification.object as? InputTextView,
inputTextView === messageInputBar.inputTextView
else {
return
}
messagesCollectionView.scrollToLastItem()
}
}