Pagination with Firebase firestore - swift 4 - swift

I'm trying to paginate data (infinitely scroll my tableview) using firestore. I've integrated the code google gives for pagination as best I can, but I'm still having problems getting the data to load in correctly.
The initial set of data loads into the tableview as wanted. Then, when the user hits the bottom of the screen, the next "x" amount of items are load in. But when the user hits the bottom of the screen the second time, the same "x" items are simply appended to the table view. The same items keep getting added indefinitely.
So its the initial 3 "ride" objects, followed by the next 4 "ride" objects repeating forever.
123 4567 4567 4567 4567...
How do I get the data to load in correctly?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
if offsetY > contentHeight - scrollView.frame.height {
// Bottom of the screen is reached
if !fetchingMore {
beginBatchFetch()
}
}
}
func beginBatchFetch() {
// Array containing "Ride" objcets is "rides"
fetchingMore = true
// Database reference to "rides" collection
let ridesRef = db.collection("rides")
let first = ridesRef.limit(to: 3)
first.addSnapshotListener { (snapshot, err) in
if let snapshot = snapshot {
// Snapshot isn't nil
if self.rides.isEmpty {
// rides array is empty (initial data needs to be loaded in).
let initialRides = snapshot.documents.compactMap({Ride(dictionary: $0.data())})
self.rides.append(contentsOf: initialRides)
self.fetchingMore = false
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
self.tableView.reloadData()
})
print("first rides loaded in")
}
} else {
// Error
print("Error retreiving rides: \(err.debugDescription)")
return
}
// reference to lastSnapshot
guard let lastSnapshot = snapshot!.documents.last else{
// The collection is empty
return
}
let next = ridesRef.limit(to: 4).start(afterDocument: lastSnapshot)
next.addSnapshotListener({ (snapshot, err) in
if let snapshot = snapshot {
if !self.rides.isEmpty {
let newRides = snapshot.documents.compactMap({Ride(dictionary: $0.data())})
self.rides.append(contentsOf: newRides)
self.fetchingMore = false
DispatchQueue.main.asyncAfter(deadline: .now() + 7, execute: {
self.tableView.reloadData()
})
print("new items")
return
}
} else {
print("Error retreiving rides: \(err.debugDescription)")
return
}
})
}
}

So here's the solution I've come up with! It is very likely that this solution makes multiple calls to firestore, creating a large bill for any real project, but it works as a proof of concept I guess you could say.
If you have any recommendations or edits, please feel free to share!
var rides = [Ride]()
var lastDocumentSnapshot: DocumentSnapshot!
var fetchingMore = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
//print("offsetY: \(offsetY) | contHeight-scrollViewHeight: \(contentHeight-scrollView.frame.height)")
if offsetY > contentHeight - scrollView.frame.height - 50 {
// Bottom of the screen is reached
if !fetchingMore {
paginateData()
}
}
}
// Paginates data
func paginateData() {
fetchingMore = true
var query: Query!
if rides.isEmpty {
query = db.collection("rides").order(by: "price").limit(to: 6)
print("First 6 rides loaded")
} else {
query = db.collection("rides").order(by: "price").start(afterDocument: lastDocumentSnapshot).limit(to: 4)
print("Next 4 rides loaded")
}
query.getDocuments { (snapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
} else if snapshot!.isEmpty {
self.fetchingMore = false
return
} else {
let newRides = snapshot!.documents.compactMap({Ride(dictionary: $0.data())})
self.rides.append(contentsOf: newRides)
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
self.tableView.reloadData()
self.fetchingMore = false
})
self.lastDocumentSnapshot = snapshot!.documents.last
}
}
}

A little late in the game, but I would like to share how I do it, using the query.start(afterDocument:) method.
class PostsController: UITableViewController {
let db = Firestore.firestore()
var query: Query!
var documents = [QueryDocumentSnapshot]()
var postArray = [Post]()
override func viewDidLoad() {
super.viewDidLoad()
query = db.collection("myCollection")
.order(by: "post", descending: false)
.limit(to: 15)
getData()
}
func getData() {
query.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
querySnapshot!.documents.forEach({ (document) in
let data = document.data() as [String: AnyObject]
//Setup your data model
let postItem = Post(post: post, id: id)
self.postArray += [postItem]
self.documents += [document]
})
self.tableView.reloadData()
}
}
}
func paginate() {
//This line is the main pagination code.
//Firestore allows you to fetch document from the last queryDocument
query = query.start(afterDocument: documents.last!)
getData()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postArray.count
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Trigger pagination when scrolled to last cell
// Feel free to adjust when you want pagination to be triggered
if (indexPath.row == postArray.count - 1) {
paginate()
}
}
}
Result like so:
Here is a reference.

My solution was similar to #yambo, however, I tried to avoid making extra calls to the database. After the first call to the database, I get back 10 objects and when it is time to load the new page I kept a reference of how many objects and I checked if the count + 9 is in the range of my new count.
#objc func LoadMore() {
let oldCount = self.uploads.count
guard shouldLoadMore else { return }
self.db.getNextPage { (result) in
switch result {
case .failure(let err):
print(err)
case .success(let newPosts):
self.uploads.insert(contentsOf: newPosts, at: self.uploads.count)
if oldCount...oldCount+9 ~= self.uploads.count {
self.shouldLoadMore = false
}
DispatchQueue.main.async {
self.uploadsView.collectionView.reloadData()
}
}
}
}

Simple, Fast and easy way is...
class FeedViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, FeedCellDelegate {
private var quotes = [Quote]() {
didSet{ tbl_Feed.reloadData() }
}
var quote: Quote?
var fetchCount = 10
#IBOutlet weak var tbl_Feed: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
fetchPost()
}
// MARK: - API
func fetchPost() {
reference(.Quotes).limit(to: getResultCount).getDocuments { (snapshot, error) in
guard let documents = snapshot?.documents else { return }
documents.forEach { (doc) in
let quotes = documents.map {(Quote(dictionary: $0.data()))}
self.quotes = quotes
}
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let currentOffset = scrollView.contentOffset.y
let maxxOffset = scrollView.contentSize.height - scrollView.frame.size.height
if maxxOffset - currentOffset <= 300 { // Your cell size 300 is example
fetchCount += 5
fetchPost()
print("DEBUG: Fetching new Data")
}
}
}

Related

Swift pagination issue

I'm working on a chat app and the issue I'm having is with pagination. I'm able to load the first batch of documents and when I scroll to the top to load the next batch, it loads but instead of keeping the first batch of documents, it removes them. I'm not sure what I'm missing or doing wrong. Any help is greatly appreciated. Here is my code:
VC
var messages = [Message]()
var lastDocument: DocumentSnapshot? = nil
var fetchMoreDocs = false
func fetchChatMessages() {
fetchMoreDocs = true
var query: Query!
if messages.isEmpty {
query = MESSAGES_COLLECTION.document(currentUID!).collection(user.memberId).order(by: TIMESTAMP).limit(to: 5)
} else {
query = MESSAGES_COLLECTION.document(currentUID!).collection(user.memberId).order(by: TIMESTAMP).start(afterDocument: self.lastDocument!).limit(to: 5)
}
query.addSnapshotListener { (snapshot, error) in
if let error = error {
print("Error..\(error.localizedDescription)")
} else if snapshot!.isEmpty {
self.fetchMoreDocs = false
return
} else {
self.messages = Message.parseData(snapshot: snapshot)
DispatchQueue.main.async {
self.chatRoomTV.reloadData()
self.scrollToBottom()
self.fetchMoreDocs = false
}
}
self.lastDocument = snapshot!.documents.last
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let position = scrollView.contentOffset.y
let maxOffset = scrollView.contentSize.height - scrollView.frame.size.height
if maxOffset - position <= -50 {
fetchChatMessages()
}}
func scrollToBottom() {
if messages.count > 0 {
let index = IndexPath(row: messages.count - 1, section: 0)
chatRoomTV.scrollToRow(at: index, at: UITableView.ScrollPosition.bottom, animated: false)
}}

Firestore pagination using MVVM architecture swift

I don't quite understand what I am doing wrong since I am very new to MVVM. It worked in MVC architecture. I've setup my VM and am able to get the first set of results and even then that's not working properly. I get 4 results instead of 10 which is what LOADLIMIT is set as. I was able to get it to work in an MVC architecture without any issues. The VM function which triggers the query is called multiple (3) times instead of just once i.e. even prior to scrolling.
Here is my VM:
enum FetchRestaurant {
case success
case error
case location
case end
}
class ListViewModel {
let restaurant: [Restaurant]?
let db = Firestore.firestore()
var restaurantArray = [Restaurant]()
var lastDocument: DocumentSnapshot?
var currentLocation: CLLocation?
typealias fetchRestaurantCallback = (_ restaurants: [Restaurant]?, _ message: String?, _ status: FetchRestaurant) -> Void
var restaurantFetched: fetchRestaurantCallback?
var fetchRestaurant: FetchRestaurant?
init(restaurant: [Restaurant]) {
self.restaurant = restaurant
}
func fetchRestaurantCallback (callback: #escaping fetchRestaurantCallback) {
self.restaurantFetched = callback
}
func fetchRestaurants(address: String) {
print("address received: \(address)")
getLocation(from: address) { location in
if let location = location {
self.currentLocation = location
self.queryGenerator(at: location)
} else {
self.restaurantFetched?(nil, nil, .location)
}
}
}
func queryGenerator(at location: CLLocation) {
var query: Query!
if restaurantArray.isEmpty {
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).limit(to: Constants.Mealplan.LOADLIMIT)
} else {
print("last document:\(String(describing: lastDocument?.documentID))")
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).start(afterDocument: lastDocument!).limit(to: Constants.Mealplan.LOADLIMIT)
}
batchFetch(query: query)
}
func batchFetch(query: Query) {
query.getDocuments { (querySnapshot, error) in
if let error = error {
self.restaurantFetched?(nil, error.localizedDescription, .error)
} else if querySnapshot!.isEmpty {
self.restaurantFetched?(nil, nil, .end)
} else if !querySnapshot!.isEmpty {
let queriedRestaurants = querySnapshot?.documents.compactMap { querySnapshot -> Restaurant? in
return try? querySnapshot.data(as: Restaurant.self)
}
guard let restaurants = queriedRestaurants,
let currentLocation = self.currentLocation else {
self.restaurantFetched?(nil, nil, .end)
return }
self.restaurantArray.append(contentsOf: self.applicableRestaurants(allQueriedRestaurants: restaurants, location: currentLocation))
DispatchQueue.main.asyncAfter(deadline: .now(), execute: {
self.restaurantFetched?(self.restaurantArray, nil, .success)
})
self.lastDocument = querySnapshot!.documents.last
}
}
}
func getLocation(from address: String, completionHandler: #escaping (_ location: CLLocation?) -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location else {
completionHandler(nil)
return
}
completionHandler(location)
}
}
}
And in the VC viewDidLoad:
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
I would really appreciate links or resources for implementing MVVM in Swift for a Firestore back-end. I'm coming up short on searches here and on Google. Even tried medium.
EDIT
class ListViewController: UITableViewController {
lazy var loadingShimmer: UIImageView = {
let image = UIImage(named: "shimmer_background")
let imageview = UIImageView(image: image)
imageview.contentMode = .top
imageview.translatesAutoresizingMaskIntoConstraints = false
return imageview
}()
var restaurantArray = [Restaurant]()
var planDictionary = [String: Any]()
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
setupTable()
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = false
}
func setupTable() {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "Restaurant", style: .plain, target: nil, action: nil)
tableView.register(RestaurantCell.self, forCellReuseIdentifier: "Cell")
tableView.delegate = self
tableView.dataSource = self
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: -navigationBarHeight, right: 0)
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.addSubview(loadingShimmer)
loadingShimmer.topAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.topAnchor).isActive = true
loadingShimmer.leadingAnchor.constraint(equalTo: tableView.leadingAnchor).isActive = true
loadingShimmer.trailingAnchor.constraint(equalTo: tableView.trailingAnchor).isActive = true
loadingShimmer.startShimmering()
initialSetup()
}
func initialSetup() {
let addressOne = planDictionary["addressOne"] as! String + ", "
let city = planDictionary["city"] as! String + ", "
let postalCode = planDictionary["postalCode"] as! String
currentAddress = addressOne + city + postalCode
setupSearch()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching {
print("\(fetchMore), \(reachedEnd)")
if !fetchMore && !reachedEnd {
if let address = self.currentAddress {
print("address sent: \(address)")
listViewModel.fetchRestaurants(address: address)
}
}
}
}
}
That you're only getting back 4 results instead of 10 is not due to a faulty query or get-document request—those are coded properly. You're either losing documents when you parse them (some are failing Restaurant initialization), Constants.Mealplan.LOADLIMIT is wrong, or there aren't more than 4 documents in the collection itself that satisfy the query.
That the query is executed 3 times instead of once is also not due to anything in this code—viewDidLoad is only called once and geocodeAddressString only returns once. You're making a fetch request elsewhere that we can't see.
In the batchFetch method, you have a guard that returns out of the function without ever calling its completion handler. This will leave the UI in a state of limbo. I'd recommend always calling the completion handler no matter why the function returns.
You never manage the document cursor. If the get-document return has less documents than the load limit, then nil the last-document cursor. This way, when you attempt to get the next page of documents, guard against a nil cursor and see if there is even more to fetch.
There's no need to pass in an empty array and have your function fill it; simply construct and return an array of results within ListViewModel itself.
We can't see how you trigger pagination. Is it through a scroll delegate when the user reaches the bottom or through a button tap, for example? If it's through a scroll delegate, then I'd disable that for now and see how many returns you get—I suspect one, instead of 3.
What is the particular reason you've ditched MVC for MVVM here? With MVC, you can get pagination up with just a few lines of code. I think MVVM is overkill for iOS applications and would advise against using it unless you have a compelling reason.

Firestore chat application displaying messaged users error

I have added chatting/messaging to my application. I can go to a users profile, select message and message them.
I also added a tab that show all the users I messaged which is in a table view and you can click a user and see messages between you and them.
The issue happens when I open this tab to see the users Ive messaged when I've messaged more than 10 users. The simulator crashes and I get this error:
"Thread 1: Exception: "Invalid Query. 'in' filters support a maximum of 10 elements in the value array."
here is my code:
import UIKit
import FirebaseFirestore
class MessagingVC: UIViewController {
#IBOutlet weak var tableView: UITableView! { didSet {
tableView.tableFooterView = UIView()
tableView.contentInset.top = 10
}}
#IBOutlet weak var noDataLabel: UILabel!
private let db = Firestore.firestore()
var users = [AppUser]()
var user: AppUser {
return UserManager.currentUser!
}
private var lastMessageDict: [String: Date]?
private var unreadMessageDict: [String: Bool]?
var chatListener: ListenerRegistration?
override func viewDidLoad() {
super.viewDidLoad()
getChats()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
noDataLabel.isHidden = true
// remove any blocked users when entering the screen.
users.removeAll() { self.user.blockedUsers?.contains($0.uid) == true }
// filter hidden users
users = users.filter { $0.isHidden ?? false == false }
tableView.reloadData()
// getChats()
}
// Get all users id that the current user has been chating with.
func getChats() {
let chatCollection = db.collection("chat")
chatListener = chatCollection.whereField("participants", arrayContains: user.uid).addSnapshotListener { [unowned self] (querySnapshot, error) in
guard error == nil else {
print(error!.localizedDescription)
return
}
var userChatIds = [String]()
self.lastMessageDict = [String: Date]()
self.unreadMessageDict = [String: Bool]()
for chat in querySnapshot?.documents ?? [] {
let chatData = chat.data()
if let participants = chatData["participants"] as? [String] {
for particiant in participants {
if particiant != self.user.uid,
!(self.user.blockedUsers?.contains(particiant) == true),
!userChatIds.contains(particiant) {
userChatIds.append(particiant)
if let lastMessageDate = chatData["last message"] as? Timestamp {
self.lastMessageDict![particiant] = lastMessageDate.dateValue()
}
if let unreadMessageDict = chatData["unread message"] as? [String: Bool],
let unreadMesage = unreadMessageDict[self.user.uid] {
self.unreadMessageDict![particiant] = unreadMesage
}
}
}
}
}
if !userChatIds.isEmpty {
self.getChatsInfo(chatIds: userChatIds)
} else {
self.tableView.reloadData()
self.noDataLabel.isHidden = self.users.count > 0
}
}
}
func getChatsInfo(chatIds: [String]) {
getUsersForChat(chatIds) { (users, error) in
guard error == nil else {
print(error?.localizedDescription ?? "")
return
}
for user in users {
if let index = self.users.firstIndex(of: user) {
self.users[index] = user
} else {
self.users.append(user)
}
}
// self.users = users
self.users.sort { (first, second) -> Bool in
let firstDate = self.lastMessageDict?[first.uid]
let secondDate = self.lastMessageDict?[second.uid]
if firstDate == nil { return false }
else if secondDate == nil { return true }
else {
return firstDate! > secondDate!
}
}
self.users = self.users.filter { $0.isHidden ?? false == false }
self.tableView.reloadData()
self.noDataLabel.isHidden = self.users.count > 0
}
}
func getUsersForChat(_ ids: [String], completion:#escaping (_ users: [AppUser], _ error: Error?)->()) {
var allUsers = [AppUser]()
let allids = self.users.map { $0.uid }
let ids = ids.filter { !allids.contains($0) }
if ids.count == 0 {
completion(allUsers, nil)
return
}
var error: Error?
let userCollection = db.collection("users")
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
userCollection.whereField("uid", in: ids).getDocuments {querySnapshot,err in
error = err
var users = [AppUser]()
for document in querySnapshot?.documents ?? [] {
if let restaurant = Restaurant(snapshot: document) {
users.append(restaurant)
}
}
allUsers.append(contentsOf: users)
dispatchGroup.leave()
}
dispatchGroup.enter()
let userCollection2 = db.collection("users2")
userCollection2.whereField("uid", in: ids).getDocuments {querySnapshot,err in
error = err
var users = [AppUser]()
for document in querySnapshot?.documents ?? [] {
if let user = AppUser(snapshot: document) {
users.append(user)
}
}
allUsers.append(contentsOf: users)
dispatchGroup.leave()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion(allUsers, error)
}
}
deinit {
chatListener?.remove()
}
}
extension MessagingVC: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as! MessagingTableViewCell
let user = users[indexPath.row]
cell.nameLabel.text = user.firstName + " " + user.lastName
if let unreadMessage = unreadMessageDict?[user.uid],
unreadMessage == true {
cell.unreadMessageIndicatorLabel.isHidden = false
} else {
cell.unreadMessageIndicatorLabel.isHidden = true
}
cell.photoImageView.image = nil
user.getProfileImage { (image) in
DispatchQueue.main.async {
if let cell = tableView.cellForRow(at: indexPath) as? MessagingTableViewCell {
cell.photoImageView.image = image
}
}
}
if let rest = user as? Restaurant {
cell.categoryImageView.image = UIImage(named: rest.Categories1)
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let user = users[indexPath.row]
// unreadMessageDict?[user.uid] = false
performSegue(withIdentifier: "messagingToChatSegue", sender: user)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "messagingToChatSegue" {
let chatVC = segue.destination as! ChatViewController
let otherUser = sender as! AppUser
chatVC.currentUser = user
chatVC.otherUser = otherUser
chatVC.channelId = ClickedResturantVC.generateIdForMessages(user1Id: user.uid, user2Id: otherUser.uid)
}
}
}
Here is a picture of the error I am getting
Here is a picture of my Firestore database
The error message is telling you that Firestore doesn't support more than 10 items in the ids array that you pass to this query filter:
userCollection.whereField("uid", in: ids)
According to the documentation for this sort of query:
Note the following limitations for in and array-contains-any:
in and array-contains-any support up to 10 comparison values.
If you need more than 10, you will need to batch them into multiple queries.

How do I hide the activity indicator once there is no more data to load?

I am paginating data from Firestore and I am able to get that to work.
Here is the paginating query:
if restaurantArray.isEmpty {
query = db.collection("Restaurant_Data").limit(to: 4)
} else {
query = db.collection("Restaurant_Data").start(afterDocument: lastDocument!).limit(to: 4)
}
query.getDocuments { (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
} else if querySnapshot!.isEmpty {
self.fetchMore = false
return
} else {
if (querySnapshot!.isEmpty == false) {
let allQueriedRestaurants = querySnapshot!.documents.compactMap { (document) -> Restaurant in (Restaurant(dictionary: document.data(), id: document.documentID)!)}
guard let location = self.currentLocation else { return }
self.restaurantArray.append(contentsOf: self.applicableRestaurants(allQueriedRestaurants: allQueriedRestaurants, location: location))
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
self.tableView.reloadData()
self.fetchMore = false
})
self.lastDocument = querySnapshot!.documents.last
}
}
}
The pagination is triggered when the user drags the table view up:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching{
if !fetchMore { // excluded reachedEnd Bool
if let location = self.currentLocation {
queryGenerator(searched: searchController.isActive, queryString: searchController.searchBar.text!.lowercased(), location: location)
}
}
}
}
I also added an activity indicator to the bottom and that works as expected. Here is the code for that:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastSectionIndex = tableView.numberOfSections - 1
let lastRowIndex = tableView.numberOfRows(inSection: lastSectionIndex) - 1
if indexPath.section == lastSectionIndex && indexPath.row == lastRowIndex {
// print("this is the last cell")
let spinner = UIActivityIndicatorView(style: .medium)
spinner.startAnimating()
spinner.frame = CGRect(x: CGFloat(0), y: CGFloat(0), width: tableView.bounds.width, height: CGFloat(44))
tableView.tableFooterView = spinner
tableView.tableFooterView?.isHidden = false
}
}
However, once the bottom of the table view is reached and there is no more data available, the indicator is still showing and I dont know how to get it to not show.
I tried using a variable to check if the Firestory query is done but my implementation is probably wrong so I cannot get it to work.
If you know the moment when there is no more data available, you set the tableView footer view to nil or hidden = true, see below
tableView.tableFooterView?.isHidden = true
or
tableView.tableFooterView = nil
before you call self.tableView.reloadData()
If you do not know it, see code below
query.getDocuments { (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
} else if querySnapshot!.isEmpty {
// Here you know when there is no more data
self.fetchMore = false
self.tableView.tableFooterView = nil // or self.tableView.tableFooterView?.isHidden = true
self.tableView.reloadData()
return
} else {
if (querySnapshot!.isEmpty == false) {
let allQueriedRestaurants = querySnapshot!.documents.compactMap { (document) -> Restaurant in (Restaurant(dictionary: document.data(), id: document.documentID)!)}
guard let location = self.currentLocation else { return }
self.restaurantArray.append(contentsOf: self.applicableRestaurants(allQueriedRestaurants: allQueriedRestaurants, location: location))
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
self.tableView.reloadData()
self.fetchMore = false
})
self.lastDocument = querySnapshot!.documents.last
}
}
}

How to stop Firestore pagination when searched data are loaded in tableView

I use infinite scroll to populate my Firestore data in tableView.Also I have a searchBar for searching.
i paginate my data successfully, but when I search something and reload the tableView with the data that have been found , the pagination starts again after I scroll. It's not possible to disable the scrolling because data may be more than the screen height.
Below I provide my code.
var fetchMoreIngredients = false
var reachEnd = false
let leadingScreensForBatching: CGFloat = 10.0
//When user scrolls down it begins to fetch more.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching{
if !fetchMoreIngredients && !reachEnd{
print(fetchMoreIngredients)
beginBatchFetch()
}
}
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
guard !searchText.isEmpty else {
ingredientsArray.removeAll()
beginBatchFetch()
return
}
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
let text = searchBar.text!.lowercased()
searchIngredients(text: text)
self.searchBarIngredient.endEditing(true)
print("\(searchIngredients(text: text))")
}
func beginBatchFetch() {
let settings = FirestoreSettings()
settings.isPersistenceEnabled = false
fetchMoreIngredients = true
let db = Firestore.firestore()
db.settings = settings
var query: Query!
if ingredientsArray.isEmpty {
SVProgressHUD.show()
query = db.collection("Ingredients").limit(to: 4)
print("First 4 ingredient loaded")
} else {
query = db.collection("Ingredients").start(afterDocument: lastDocument).limit(to: 4)
print("Next 4 ingredient loaded")
}
query.getDocuments { (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else if querySnapshot!.isEmpty {
self.fetchMoreIngredients = false
return
} else {
if (querySnapshot!.isEmpty == false){
let res = querySnapshot!.documents.compactMap({Ingredients(dictionary: $0.data())})
self.ingredientsArray.append(contentsOf: res)
self.tableView.reloadData()
self.fetchMoreIngredients = false
SVProgressHUD.dismiss()
}
self.lastDocument = querySnapshot!.documents.last
}
}
}
func searchIngredients(text: String){
fetchMoreIngredients = false
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
if (querySnapshot!.isEmpty == false){
self.searchedIngredientsArray = querySnapshot!.documents.compactMap({Ingredients(dictionary: $0.data())})
self.ingredientsArray = self.searchedIngredientsArray
self.tableView.reloadData()
}else{
print("No Ingredients were found")
}
}
}
}
Thanks in advance!
SOLUTION
I finally found the solution. In the searchIngredients(text: String) function I should have declared the fetchMoreIngredients as true in the beginning and within the closure.
func searchIngredients(text: String){
fetchMoreIngredients = true
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
if (querySnapshot!.isEmpty == false){
self.searchedIngredientsArray = querySnapshot!.documents.compactMap({Ingredients(dictionary: $0.data())})
self.ingredientsArray = self.searchedIngredientsArray
self.tableView.reloadData()
self.fetchMoreIngredients = true
}else{
print("No Ingredients were found")
}
}
}
}