Search code examples
iosswiftfirebase-realtime-databasepaginationuisearchcontroller

FirebaseDatabase -How to Paginate when using a UISearchController


As the user types text into the searchBar the UISearchController has a delegate method to update search results:

func updateSearchResults(for searchController: UISearchController) {

    guard let searchText = searchController.searchBar.text?.lowercased() else { return }

    Database...usersRef
        .queryOrdered(byChild: "username")
        .queryStarting(atValue: searchText)
        .queryEnding(atValue: searchText+"\u{f8ff}")
        .observe( .childAdded, with: { [weak self](snapshot) in

            let key = snapshot.key

            guard let dict = snapshot.value as? [String: Any] else { return }

            let user = User(userId: key, dict: dict)
            self?.datasource.append(user)
        })
}

That works fine.

When I normally paginate I use this procedure:

var startKey: String?

func handlePaginationForPosts() {

    if startKey == nil {

        Database...PostsRef
            .queryOrderedByKey()
            .queryLimited(toLast: 10)
            .observeSingleEvent(of: .value, with: { [weak self] (snapshot) in

                guard let children = snapshot.children.allObjects.first as? DataSnapshot else { return }

                if snapshot.childrenCount > 0 {
                    for child in snapshot.children.allObjects as! [DataSnapshot] {

                        let postId = child.key

                        if child.key != self?.startKey {

                            guard let dict = child.value as? [String:Any] else { return }

                            let post = Post(postId: postId, dict: dict)

                            self?.datasource.insert(post, at: 0)
                        }
                    }
                    self?.startKey = children.key
                }
            })

    } else {

        let lastIndex = datasource.count

        Database...PostsRef
            .queryOrderedByKey()
            .queryEnding(atValue: startKey!)
            .queryLimited(toLast: 11)
            .observeSingleEvent(of: .value, with: { [weak self] (snapshot) in

                guard let children = snapshot.children.allObjects.first as? DataSnapshot else { return }

                if snapshot.childrenCount > 0 {
                    for child in snapshot.children.allObjects as! [DataSnapshot] {

                        let postId = child.key

                        if child.key != self?.startKey {

                            guard let dict = child.value as? [String:Any] else { return }

                            let post = Post(postId: postId, dict: dict)
                            // I run a check to make sure the datasource doesn't contain the post before adding it
                            self?.datasource.insert(post, at: lastIndex)
                        }
                    }
                    self?.startKey = children.key
                }
            })
    }
}

The problem here is when running a search I use:

.queryStarting(atValue: searchText)
.queryEnding(atValue: searchText+"\u{f8ff}")

But when paginating a post I use:

.queryOrderedByKey()
.queryEnding(atValue: startKey!) ...

self?.datasource.insert(post, at: lastIndex)

The startKey is the first key in the snapshot.children.allObjects.first and the lastIndex is the datasource.count.

Considering the search query is based on the search text and not a key, how can I paginate when I'm already using .queryEnding(atValue: searchText+"\u{f8ff}") instead of .queryEnding(atValue: startKey!)?

I need to track the key that was pulled from the db so that when I paginate I can run the next set of results from that particular key.


Solution

  • Firebase Database queries can only order/filter on a single property.

    So what you can do is filter for the search criteria, and then limit to the firsts N results.

    What you can't do is filter for the search criteria, skip the first N results, and get the next page.

    The closest you can get, and something regularly done for cases such as this, is retrieve the first 2*N results when you need to show page 2. This wastes some bandwidth though, so you'll have to trade that off against how useful the pagination is.