Here is my asset fetching function.
You can see I am using limit
logic, at the line limit(to: AlbumRepository.assetPageSize)
.
Then you will need an "index" to fetch documents right before/after your "index".
Ex: your message
will have a prop named created_date
.
We consider that asset is your message, albumDocId is your chatRoomId, asset_created
is your message's timestamp
.
So we will:
- Query all documents that have
created_date
older than "index" (index here is a Date in second or TimeStamp). Line: whereField("asset_created", isLessThanOrEqualTo: lastCreatedForQuery)
- Sort the result from newest to oldest by using
created_date
. Line: order(by: "asset_created", descending: true)
to sort by asset_created
ASC. (ASC when you display newest at the bottom, DESC when you display newest at the top)
- Now we limit the number of returned documents to x items. Line:
limit(to: AlbumRepository.assetPageSize)
, my value is 20 items.
After you get the first batch with index
is now, just save the created_date
of the last document in the returned list (from query's response). Then put in whereField("asset_created", isLessThanOrEqualTo: {index})
. Last item on the list will be the oldest message.
// MARK: - Asset repo functions
static let assetPageSize = 20
func fetchAsset(albumDocId id: String, lastCreated: TimeInterval?, done: @escaping ([FSAssetModel]) -> Void) {
let lastCreatedForQuery = lastCreated ?? Date().timeIntervalSince1970
FirebaseFirestoreManager.db
.collection(tableName)
.document(id)
.collection("asset")
.whereField("asset_created", isLessThan: lastCreatedForQuery)
.order(by: "asset_created", descending: false)
.limit(to: AlbumRepository.assetPageSize)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
done([])
} else {
var finalResult: [FSAssetModel] = []
for document in querySnapshot!.documents {
if let asset = FSAssetModel(JSON: document.data()) {
asset.doc_id = document.documentID
finalResult.append(asset)
}
}
done(finalResult)
}
}
}
Next, in your message screen, you have an array messages
, to use it as dataSource.
And, private var _lastCreatedOfMessage: Date?
// When you open your message screen, you will call this function to fetch last x message,
// and the first time you call this function, you have no _lastCreatedOfMessage yet, it will be null.
// That means we will last x message from NOW
// You can check this line in the fetch method:
// let lastCreatedForQuery = lastCreated ?? Date().timeIntervalSince1970
// albumDocId: "ABCD1234" -> it will be your room id
// You will call this method anytime the user scrolls to the top/bottom to load history messages, after the `firstLoadMessages()` when you open screen.
fetchAsset(albumDocId: "ABCD1234", lastCreated: self._lastCreatedOfMessage) { [weak self] messages
guard let _self = self else, !messages.isEmpty { return }
// 1) Save your lastCreated for next fetch/query
// We order by created_date ASC so the first item will be oldest
// It will be used for next list [history message] fetching
_self.lastCreatedOfMessage = items.first!.created_date
// 2) Now we insert this message list to begin of your message list, that is dataSource for displaying
_self.yourMessageList.insert(contentsOf: messages, at: 0)
// 3) Reload your list, then scroll to the top or first indexPath
// Because you are dragging the listView down to see the older message
// That is why you use order by created_date [ASC]
_self.yourListView.reloadData() // ex: UICollectionView
_self.yourListView.scrollToItem(at: IndexPath.init(item: 0, section: 0), at: .top, animated: true)
// or more simple trick
// _self.yourListView.setContentOffset(.zero, animated: true)
}
Finally, to update your observer logic, you just need to observe the last new/changed message. Whenever you have a new incoming message, append it to your message list.
THE MOST REASONABLE LOGIC IS:
- Open screen, fetch the list of last x messages,
- When you get the list of last x messages, initialize your observer now, we will observe for any new message that has
timestamp
that is newer than the newest message from the above list.
static func observeNewMessage(roomId: String, lastTimeStamp: TimeInterval, completion: @escaping(RoomMessage?, Error?) -> Void) -> Query {
let now = Date().timeIntervalSince1970
let query = COLLECTION_ROOMS.document(roomId)
.collection("messages")
.whereField("timestamp", isGreaterThan: lastTimeStamp) // only take message that newer than lastTimeStamp
.order(by: "timestamp", descending: false)
.limit(toLast: 1)
// You will need to retain this query instance to keep observing for new message until you close message screen.
query.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { change in
let dictionary = change.document.data()
var message = RoomMessage(dictionary: dictionary)
if (change.type == .added) {
completion(message, nil)
print("Added msg: \(message.text)")
}
if (change.type == .modified) {
print("Modified msg: \(message.text)")
}
if (change.type == .removed) {
print("Removed msg: \(message.text)")
}
}
}
return query
}
How to use:
private var newMessageQueryListener: Query? // this variable is used to retain query [observeNewMessage]
// You only call this function once when open screen
func firstLoadMessages() {
showLoader(true)
guard let room = room else, let roomId = room.recentMessage.roomID { return }
fetchAsset(albumDocId: "ABCD1234", lastCreated: self._lastCreatedOfMessage) { [weak self] messages
guard let _self = self else, !messages.isEmpty { return }
// 1) Save your lastCreated for next fetch/query
// We order by created_date ASC so the first item will be oldest
// It will be used for next list [history message] fetching
_self.lastCreatedOfMessage = items.first!.timestamp
// 2) Now we insert this message list to begin of your message list, that is dataSource for displaying
_self.yourMessageList.insert(contentsOf: messages, at: 0)
// 3) Reload your list, then scroll to the top or LAST indexPath
// Because user just open screen not loading older messages
// ALWAYS call reload data before you do some animation,...
_self.yourListView.reloadData() // ex: UICollectionView
// Scroll to last index with position: bottom for newest message
let lastIndex = _self.messages.count - 1
_self.yourListView.scrollToItem(at: IndexPath.init(item: lastIndex, section: 0), at: .botom, animated: true)
// 4) Setup your observer for new message here
let lastTimeStamp = items.last!.timestamp
_self.makeNewMessageObserver(roomId: roomId, lastTimeStamp: lastTimeStamp)
}
}
private func makeNewMessageObserver(roomId: String, lastTimeStamp: TimeInterval) {
self.newMessageQueryListener = RoomService.observeNewMessage(roomId: roomId, lastTimeStamp: lastTimeStamp) { [weak self] newMess, error in
// DO NOT CALL self or REFER self in the block/closure IF you dont know its life-cycle.
// [weak self] will make an optional refering to self to prevent memory-leak.
guard let _self = self else { return } // here we check if self(MessageScreen) is null or not, if not null we continue
_self.messages.append(newMess) // ASC so the last item in list will be newest message
// Reload then scroll to the bottom for newest message
DispatchQueue.main.async {
_self.collectionView.reloadData()
let lastIndex = _self.messages.count - 1
_self.collectionView.scrollToItem(at: IndexPath(item: lastIndex, section: 0), at: .bottom, animated: true)
}
}
}
The rule is: Fetch a list, but observe one.