I have a chat and when i scroll down to fetch older messages i want the collectionView to stay still and allow the user to manually scroll the older message who just loaded like in messenger. With my collectionView it automatically scroll to the top of the new items. I have added a method inside my scrollViewDidEndDragging but there is 2 problems:
- It's glitchy meaning it scroll to the top of the new data then scroll back to the "previous position".
- It's not exactly moving back to the previous position, the oldest message before loading the older data is not as the same place, it's almost in the center instead of staying on top. Here's my code:
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset.y
if contentOffset <= -40 {
self.collectionView.refreshControl?.beginRefreshing()
self.fetchMessages()
let beforeTableViewContentHeight = collectionView.contentSize.height
let beforeTableViewOffset = collectionView.contentOffset.y
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.collectionView.layer.layoutIfNeeded()
let insertCellHeight = beforeTableViewOffset + (self.collectionView.contentSize.height - beforeTableViewContentHeight)
let newOffSet = CGPoint(x: 0, y: insertCellHeight)
self.collectionView.contentOffset = newOffSet
}
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension RoomMessageViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: view.frame.width, height: 10)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return .init(top: 16, left: 0, bottom: 16, right: 0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 50)
let estimatedSizeCell = RoomMessageCell(frame: frame)
estimatedSizeCell.roomMessage = chatMessages[indexPath.section][indexPath.row]
estimatedSizeCell.layoutIfNeeded()
let targetSize = CGSize(width: view.frame.width, height: 1000)
let estimatedSize = estimatedSizeCell.systemLayoutSizeFitting(targetSize)
return CGSize(width: view.frame.width, height: estimatedSize.height)
}
}
[Updated code#1]
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.contentOffset.y <= -50 {
self.collectionView.reloadData()
self.fetchMessages()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
let previousContentSize = self.collectionView.contentSize
self.collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.collectionViewLayout.prepare()
self.collectionView.layoutIfNeeded()
let newContentSize = self.collectionView.contentSize
print("previous Content Size \(previousContentSize) and new Content Size \(newContentSize)")
let contentOffset = newContentSize.height - previousContentSize.height
self.collectionView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
}
}
}
[Updated Code#2]
var lastDocumentSnapshot: DocumentSnapshot!
var isScrollBottom = false
var isFirstLoad = true
var isLoading = false
private var messages = [RoomMessage]()
private var chatMessages = [[RoomMessage]]()
override func viewDidAppear(_ animated: Bool) {
self.isScrollBottom = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !isScrollBottom {
DispatchQueue.main.async(execute: {
self.collectionView.scrollToBottom(animated: false)
self.isScrollBottom = true
self.isFirstLoad = false
})
}
}
func fetchMessages() {
var query: Query!
guard let room = room else{return}
guard let roomID = room.recentMessage.roomID else{return}
collectionView.refreshControl?.beginRefreshing()
if messages.isEmpty {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).limit(toLast: 15)
print("First 15 msg loaded")
} else {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).end(beforeDocument: lastDocumentSnapshot).limit(toLast: 15)
print("Next 15 msg loaded")
}
query.addSnapshotListener { (snapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
} else if snapshot!.isEmpty {
self.collectionView.refreshControl?.endRefreshing()
return
}
guard let lastSnap = snapshot?.documents.first else {return}
self.lastDocumentSnapshot = lastSnap
snapshot?.documentChanges.forEach({ (change) in
if change.type == .added {
let dictionary = change.document.data()
let timestamp = dictionary["timestamp"] as? Timestamp
var message = RoomMessage(dictionary: dictionary)
let date = timestamp?.dateValue()
let formatter1 = DateFormatter()
// formatter1.dateStyle = .medium
formatter1.timeStyle = .short
message.timestampHour = formatter1.string(from: date!)
message.timestampDate = date!
self.messages.append(message)
self.messages.sort(by: { $0.timeStamp.compare($1.timeStamp) == .orderedAscending })
}
self.attemptToAssembleGroupedMessages { (assembled) in
if assembled {
}
}
})
self.collectionView.refreshControl?.endRefreshing()
self.lastDocumentSnapshot = snapshot?.documents.first
}
}
// MARK: - Helpers
func configureUI() {
// collectionView.alwaysBounceVertical = true
self.hideKeyboardOnTap()
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.sectionHeadersPinToVisibleBounds = true
}
let refreshControl = UIRefreshControl()
collectionView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
}
fileprivate func attemptToAssembleGroupedMessages(completion: (Bool) -> ()){
chatMessages.removeAll()
let groupedMessages = Dictionary(grouping: messages) { (element) -> Date in
return element.timestampDate.reduceToMonthDayYear() }
// provide a sorting for the keys
let sortedKeys = groupedMessages.keys.sorted()
sortedKeys.forEach { (key) in
let values = groupedMessages[key]
chatMessages.append(values ?? [])
self.collectionView.reloadData()
}
completion(true)
}
@objc func handleRefresh() {
}
}
extension RoomMessageViewController {
//NEKLAS METHOD
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let contentOffset = scrollView.contentOffset.y
var lastContentSize = scrollView.contentSize
var currentOffset = scrollView.contentOffset
if contentOffset <= -50 {
self.isLoading = true
self.collectionView.reloadData()
self.fetchMessages()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
self.collectionView.layer.layoutIfNeeded()
let newContentSize = self.collectionView.contentSize
let delta = newContentSize.height - lastContentSize.height
lastContentSize = self.collectionView.contentSize
if delta > 0 {
currentOffset.y = currentOffset.y + delta
self.collectionView.setContentOffset(currentOffset, animated: false)
if self.isLoading { return }
self.isLoading = false
}
}
}
}
//ANOTHER METHOD (More precise but has some bug)
// override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// if scrollView.contentOffset.y <= -50 {
// self.collectionView.reloadData()
// self.fetchMessages()
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
// let previousContentSize = self.collectionView.contentSize
// self.collectionView.collectionViewLayout.invalidateLayout()
// self.collectionView.collectionViewLayout.prepare()
// self.collectionView.layoutIfNeeded()
// let newContentSize = self.collectionView.contentSize
// print("previous Content Size \(previousContentSize) and new Content Size \(newContentSize)")
// let contentOffset = newContentSize.height - previousContentSize.height
// self.collectionView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
// }
// }
//}
[Updated Code #3]
var currentOffset: CGPoint = .zero
var lastContentSize: CGSize = .zero
var currentPage = 1
@objc func fetchMessages() {
var query: Query!
guard let room = room else{return}
guard let roomID = room.recentMessage.roomID else{return}
collectionView.refreshControl?.beginRefreshing()
if messages.isEmpty {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).limit(toLast: 15)
print("First 15 msg loaded")
} else {
query = COLLECTION_ROOMS.document(roomID).collection("messages").order(by: "timestamp", descending: false).end(beforeDocument: lastDocumentSnapshot).limit(toLast: 15)
print("Next 15 msg loaded")
self.currentPage = self.currentPage + 1
print(self.currentPage)
}
query.addSnapshotListener { (snapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
} else if snapshot!.isEmpty {
self.collectionView.refreshControl?.endRefreshing()
return
}
guard let lastSnap = snapshot?.documents.first else {return}
self.lastDocumentSnapshot = lastSnap
snapshot?.documentChanges.forEach({ (change) in
if change.type == .added {
let dictionary = change.document.data()
let timestamp = dictionary["timestamp"] as? Timestamp
var message = RoomMessage(dictionary: dictionary)
let date = timestamp?.dateValue()
let formatter1 = DateFormatter()
formatter1.timeStyle = .short
message.timestampHour = formatter1.string(from: date!)
message.timestampDate = date!
self.messages.append(message)
self.messages.sort(by: { $0.timeStamp.compare($1.timeStamp) == .orderedAscending })
self.attemptToAssembleGroupedMessages { (assembled) in
if assembled {
}
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .milliseconds(10)) {
if self.currentPage == 1 {
self.collectionView.reloadData()
self.collectionView.scrollToBottom(animated: false)
} else {
self.collectionView.reloadData()
self.collectionView.layoutIfNeeded()
let newContentSize = self.collectionView.contentSize
let delta = newContentSize.height - self.lastContentSize.height
self.lastContentSize = self.collectionView.contentSize
if delta > 0 {
self.currentOffset.y = self.currentOffset.y + delta
self.collectionView.setContentOffset(self.currentOffset, animated: false)
}
}
self.collectionView.refreshControl?.endRefreshing()
self.lastDocumentSnapshot = snapshot?.documents.first
}
}
})
}
}
fileprivate func attemptToAssembleGroupedMessages(completion: (Bool) -> ()){
chatMessages.removeAll()
let groupedMessages = Dictionary(grouping: messages) { (element) -> Date in
return element.timestampDate.reduceToMonthDayYear() }
// provide a sorting for the keys
let sortedKeys = groupedMessages.keys.sorted()
sortedKeys.forEach { (key) in
let values = groupedMessages[key]
chatMessages.append(values ?? [])
// self.collectionView.reloadData()
}
completion(true)
}
}
extension RoomMessageViewController {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
// We capture any change of offSet/contentSize
self.lastContentSize = scrollView.contentSize
self.currentOffset = scrollView.contentOffset
}