From b6f756b27c4ee832c5485fc6e057a5079a9e92df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B9=90=E5=8D=87=E5=B9=B3?= Date: Tue, 1 Apr 2025 01:24:07 +0800 Subject: [PATCH] Add a scrollToBottom method to allow the UICollectionView to scroll to the bottom, and fix the issue of the navigation bar blur effect abruptly changing when using scrollToLastItem. --- .../AdvancedExampleViewController.swift | 4 +- .../AutocompleteExampleViewController.swift | 2 +- .../View Controllers/ChatViewController.swift | 6 +-- .../LaunchViewController.swift | 14 +++++++ .../Sources/Views/SwiftUI/MessagesView.swift | 4 +- .../MessagesViewController+Keyboard.swift | 6 +-- Sources/Views/MessagesCollectionView.swift | 42 +++++++++++++++++++ 7 files changed, 67 insertions(+), 11 deletions(-) diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index d575e64cc..07271d980 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -89,7 +89,7 @@ final class AdvancedExampleViewController: ChatViewController { DispatchQueue.main.async { self.messageList = messages self.messagesCollectionView.reloadData() - self.messagesCollectionView.scrollToLastItem() + self.messagesCollectionView.scrollToBottom() } } } @@ -194,7 +194,7 @@ final class AdvancedExampleViewController: ChatViewController { updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...") setTypingIndicatorViewHidden(isHidden, animated: animated, whilePerforming: updates) { [weak self] success in if success, self?.isLastSectionVisible() == true { - self?.messagesCollectionView.scrollToLastItem(animated: true) + self?.messagesCollectionView.scrollToBottom(animated: true) } } } diff --git a/Example/Sources/View Controllers/AutocompleteExampleViewController.swift b/Example/Sources/View Controllers/AutocompleteExampleViewController.swift index c5b2de5da..5a6eda83f 100644 --- a/Example/Sources/View Controllers/AutocompleteExampleViewController.swift +++ b/Example/Sources/View Controllers/AutocompleteExampleViewController.swift @@ -150,7 +150,7 @@ final class AutocompleteExampleViewController: ChatViewController { func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in if success, self?.isLastSectionVisible() == true { - self?.messagesCollectionView.scrollToLastItem(animated: true) + self?.messagesCollectionView.scrollToBottom(animated: true) } } } diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index aeb2171ae..981346261 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -81,7 +81,7 @@ class ChatViewController: MessagesViewController, MessagesDataSource { DispatchQueue.main.async { self.messageList = messages self.messagesCollectionView.reloadData() - self.messagesCollectionView.scrollToLastItem(animated: false) + self.messagesCollectionView.scrollToBottom(animated: false) } } } @@ -132,7 +132,7 @@ class ChatViewController: MessagesViewController, MessagesDataSource { } }, completion: { [weak self] _ in if self?.isLastSectionVisible() == true { - self?.messagesCollectionView.scrollToLastItem(animated: true) + self?.messagesCollectionView.scrollToBottom(animated: true) } }) } @@ -350,7 +350,7 @@ extension ChatViewController: InputBarAccessoryViewDelegate { inputBar.sendButton.stopAnimating() inputBar.inputTextView.placeholder = "Aa" self?.insertMessages(components) - self?.messagesCollectionView.scrollToLastItem(animated: true) + self?.messagesCollectionView.scrollToBottom(animated: true) } } } diff --git a/Example/Sources/View Controllers/LaunchViewController.swift b/Example/Sources/View Controllers/LaunchViewController.swift index 90c42b894..ba6866aaf 100644 --- a/Example/Sources/View Controllers/LaunchViewController.swift +++ b/Example/Sources/View Controllers/LaunchViewController.swift @@ -43,6 +43,20 @@ final internal class LaunchViewController: UITableViewController { title = "MessageKit" navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) navigationController?.navigationBar.tintColor = .primaryColor + if let navigationBar = self.navigationController?.navigationBar { + if #available(iOS 15.0, *) { + let appearance = UINavigationBarAppearance() + appearance.backgroundEffect = nil + appearance.backgroundColor = .white + navigationBar.standardAppearance = appearance + navigationBar.scrollEdgeAppearance = appearance + } else { + navigationBar.setBackgroundImage(UIImage(), for: .default) + navigationBar.shadowImage = UIImage() + navigationBar.barTintColor = .white + } + } + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.tableFooterView = UIView() } diff --git a/Example/Sources/Views/SwiftUI/MessagesView.swift b/Example/Sources/Views/SwiftUI/MessagesView.swift index c6f6fcbae..f3fe438f1 100644 --- a/Example/Sources/Views/SwiftUI/MessagesView.swift +++ b/Example/Sources/Views/SwiftUI/MessagesView.swift @@ -31,7 +31,7 @@ final class MessageSwiftUIVC: MessagesViewController { super.viewDidAppear(animated) // Because SwiftUI wont automatically make our controller the first responder, we need to do it on viewDidAppear becomeFirstResponder() - messagesCollectionView.scrollToLastItem(animated: true) + messagesCollectionView.scrollToBottom(animated: true) } } @@ -89,7 +89,7 @@ struct MessagesView: UIViewControllerRepresentable { private func scrollToBottom(_ uiViewController: MessagesViewController) { DispatchQueue.main.async { // The initialized state variable allows us to start at the bottom with the initial messages without seeing the initial scroll flash by - uiViewController.messagesCollectionView.scrollToLastItem(animated: self.initialized) + uiViewController.messagesCollectionView.scrollToBottom(animated: self.initialized) self.initialized = true } } diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index 974fa1077..f3ba72edc 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -64,7 +64,7 @@ extension MessagesViewController { self?.updateMessageCollectionViewBottomInset() if !(self?.maintainPositionOnInputBarHeightChanged ?? false) { - self?.messagesCollectionView.scrollToLastItem() + self?.messagesCollectionView.scrollToBottom() } } .store(in: &disposeBag) @@ -79,7 +79,7 @@ extension MessagesViewController { self?.updateMessageCollectionViewBottomInset() if !(self?.maintainPositionOnInputBarHeightChanged ?? false) { - self?.messagesCollectionView.scrollToLastItem() + self?.messagesCollectionView.scrollToBottom() } } .store(in: &disposeBag) @@ -147,6 +147,6 @@ extension MessagesViewController { else { return } - messagesCollectionView.scrollToLastItem() + messagesCollectionView.scrollToBottom() } } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index a551ef2f9..58cb6a034 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -85,6 +85,48 @@ open class MessagesCollectionView: UICollectionView { scrollToItem(at: indexPath, at: pos, animated: animated) } + + public func scrollToBottom(animated: Bool = true) { + guard let indexPath = indexPathForLastItem else { return } + + // Store the current content offset + let originalOffset = contentOffset + // Scroll to the item to get the updated contentOffset + scrollToItem(at: indexPath, at: .bottom, animated: false) + + guard let layout = collectionViewLayout as? UICollectionViewFlowLayout else { return } + + let targetOffset = contentOffset + + // Immediately reset the content offset to the original, without animation + setContentOffset(originalOffset, animated: false) + + // Get the bottom section inset + let sectionInsetBottom = layout.sectionInset.bottom + + // Get the height of the footer view + var footerHeight: CGFloat = 0 + + // Try to get the footer view's height from the layout attributes + if let footerAttributes = layout.layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath) { + footerHeight = footerAttributes.frame.height + } + // If the layout attributes are not available, try to get the height from the delegate method + else if let delegate = self.delegate as? UICollectionViewDelegateFlowLayout { + footerHeight = delegate.collectionView?(self, layout: layout, referenceSizeForFooterInSection: indexPath.section).height ?? 0 + } + // If neither method provides the height, fall back to the layout's default footer reference size + else { + footerHeight = layout.footerReferenceSize.height + } + + let totalBottomInset = sectionInsetBottom + footerHeight + + if totalBottomInset > 0 { + let adjustedOffset = CGPoint(x: targetOffset.x, y: targetOffset.y + totalBottomInset) + setContentOffset(adjustedOffset, animated: animated) + } + } public func reloadDataAndKeepOffset() { // stop scrolling