FirebaseDatabase -How to Paginate when using a UISearchController - ios

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.

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.

Related

How would I iterate over all keys to get specific child value?

I am trying to iterate over all keys under "Timetable" to get the key value and Name of those that have an approved value of "Yes".
So for the following JSON structure:
Timetable
Pikes_Lane_Primary_School_Bolton_UK
Approved: Yes
Name: Pikes Lane Primary School
Essa_Academy_Bolton_UK
Approved: No
Name: Essa Academy
Ladybridge_High_School_Bolton_UK
Approved: Yes
Name: Ladybridge High School
My desired output would be:
Pikes_Lane_Primary_School_Bolton_UK
Pikes Lane Primary School
Ladybridge_High_School_Bolton_UK
Ladybridge High School
This is the best I've managed to do over the last few hours:
let schoolDatabase = Database.database().reference().child("Timetable")
schoolDatabase.observeSingleEvent(of: .value, with: { (snapshot) in
for child in snapshot.children {
let schoolID = child as! DataSnapshot
//print(schoolID.key)
for grandchild in schoolID.children {
let varOne = grandchild as! DataSnapshot
print(varOne.key)
}
}
})
This brings back the following:
Approved
Name
Approved
Name
Approved
Name
let schoolDatabase = Database.database().reference().child("Timetable")
schoolDatabase
.queryOrdered(byChild: "Approved")
.queryEqual(toValue: "Yes")
.observeSingleEvent(of: .value, with: { (snapshot) in
let children = snapshot.children
.compactMap { $0 as? DataSnapshot }
children.forEach { tuple in
print(tuple.key)
if let tupleDictionary = tuple.value as? [String: Any] {
let name = tupleDictionary["Name"] as? String
print(name ?? "-")
}
}
}
)
Or if you are interested only in names (without key):
let schoolDatabase = Database.database().reference().child("Timetable")
schoolDatabase
.queryOrdered(byChild: "Approved")
.queryEqual(toValue: "Yes")
.observeSingleEvent(of: .value, with: { (snapshot) in
let children = snapshot.children
.compactMap { $0 as? DataSnapshot }
.compactMap { $0?.value as? [String: Any]}
.compactMap { $0["Name"] as? String }
children.forEach { name in
print(name)
}
}
)
Finally got there in the end!
let schoolDatabase = Database.database().reference().child("Timetable")
schoolDatabase.observeSingleEvent(of: .value, with: { (snapshot) in
for child in snapshot.children {
let schoolID = child as! DataSnapshot
let stringApproved = schoolID.childSnapshot(forPath: "Approved").value
let stringSchoolName = schoolID.childSnapshot(forPath: "Name").value
if stringApproved as? String == "Yes" {
print(schoolID.key)
print((stringSchoolName)!)
print((stringApproved)!)
}
}
})

Load new messages Swift 4.2 & Firebase

I have created a messaging system for my app and am paginating the messages within the chat log but I'm having an issue that if a new message is sent the user will have to leave the screen and re open the controller to view the new messages they have sent/received. I have tried to reload the collection view and observe the messages again with no luck. Any help is appreciated.
Observing the messages. With Pagination. (working great! On initial load.)
var messages = [Message]()
fileprivate func observeMessages() {
guard let uid = Auth.auth().currentUser?.uid else { return }
guard let userId = user?.uid else { return }
if currentKey == nil {
let userMessageRef = Database.database().reference().child("user-message").child(uid).child(userId).queryLimited(toLast: 10).observeSingleEvent(of: .value) { (snapshot) in
guard let first = snapshot.children.allObjects.first as? DataSnapshot else { return }
guard var allObjects = snapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (snapshot) in
let messageId = snapshot.key
let ref = Database.database().reference().child("messages").child(messageId)
ref.observe(.value, with: { (snapshot) in
guard let dict = snapshot.value as? [String: Any] else { return }
let message = Message(dictionary: dict)
self.messages.append(message)
self.messages.sort(by: { (message1, message2) -> Bool in
return message1.timeStamp.compare(message2.timeStamp) == .orderedDescending
})
self.collectionView?.reloadData()
})
})
self.currentKey = first.key
}
} else {
let userMessageRef = Database.database().reference().child("user-message").child(uid).child(userId).queryOrderedByKey().queryEnding(atValue: self.currentKey).queryLimited(toLast: 4).observeSingleEvent(of: .value) { (snapshot) in
guard let first = snapshot.children.allObjects.first as? DataSnapshot else { return }
guard var allObjects = snapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (snapshot) in
if snapshot.key != self.currentKey {
let messageId = snapshot.key
let ref = Database.database().reference().child("messages").child(messageId)
ref.observe(.value, with: { (snapshot) in
guard let dict = snapshot.value as? [String: Any] else { return }
let message = Message(dictionary: dict)
self.messages.append(message)
self.messages.sort(by: { (message1, message2) -> Bool in
return message1.timeStamp.compare(message2.timeStamp) == .orderedDescending
})
self.collectionView?.reloadData()
})
}
})
self.currentKey = first.key
}
}
}
From Firebase database documentation
In some cases you may want a callback to be called once and then immediately removed, such as when initializing a UI element that you don't expect to change. You can use the observeSingleEventOfType method to simplify this scenario: the event callback added triggers once and then does not trigger again.
I suggest you to change to observeEventType:withBlock whichs allow you to observe all changes events.
Hope this helps.
The way I set mine up was to call the function in viewDidLoad and then again in viewDidAppear. I'm still learning as well, but you may want to try that, it would probably look something like this:
override func viewDidLoad() {
super.viewDidLoad()
observeMessages(for: userID) { (messages) in
self.messages = messages
self.collectionView.reloadData()
}
}
And again in viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
observeMessages(for: userID) { (messages) in
self.messages = messages
self.collectionView.reloadData()
}
}

iOS Firebase asynchronous data retrieval into Array

I try to retrieve data from Firebase into Array. Because it runs asynchronously, the results that I want to show in my CollectionView is a delay until I switch back and forth. I am very new to asynchronous functions in iOS. Please help me to complete my code.
ref = Database.database().reference(withPath: "MyTest/Video")
ref?.observeSingleEvent(of: .value, with: { snapshot in
if !snapshot.exists() { return }
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let autoID = child.key as String //get autoID
let title = snapshot.childSnapshot(forPath: "\(autoID)/Title").value
let url = snapshot.childSnapshot(forPath: "\(autoID)/URL").value
let views = snapshot.childSnapshot(forPath: "\(autoID)/Views").value
self.arrayAllTitle.append(title as! String)
self.arrayAllId.append(url as! String)
self.arrayAllDesc.append(views as! String)
}
}
})
You need to reload the collection after you retrieve the data so after the for loop call reloadData()
for child in result {
}
self.collectionView.reloadData()
//
func getValueFromDatabase(completion: #escaping (_ status: Bool) -> Void){
ref = Database.database().reference(withPath: "MyTest/Video")
ref?.observeSingleEvent(of: .value, with: { snapshot in
if !snapshot.exists() { return }
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let autoID = child.key as String //get autoID
let title = snapshot.childSnapshot(forPath: "\(autoID)/Title").value
let url = snapshot.childSnapshot(forPath: "\(autoID)/URL").value
let views = snapshot.childSnapshot(forPath: "\(autoID)/Views").value
self.arrayAllTitle.append(title as! String)
self.arrayAllId.append(url as! String)
self.arrayAllDesc.append(views as! String)
}
completion(true)
}
else {
completion(false)
}
})
}
//
self.getValueFromDatabase { (status) in
if status {
// success
}
}
I'm working with Firebase in my project right now. I would suggest the following solution: wrap the database observer in a distinct function which gets completion block as a parameter.
func getValueFromDatabase(completion: ()->Void){
ref = Database.database().reference(withPath: "MyTest/Video")
ref?.observeSingleEvent(of: .value, with: { snapshot in
if !snapshot.exists() { return }
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let autoID = child.key as String //get autoID
let title = snapshot.childSnapshot(forPath: "\(autoID)/Title").value
let url = snapshot.childSnapshot(forPath: "\(autoID)/URL").value
let views = snapshot.childSnapshot(forPath: "\(autoID)/Views").value
self.arrayAllTitle.append(title as! String)
self.arrayAllId.append(url as! String)
self.arrayAllDesc.append(views as! String)
}
completion()
}
})
}
This way you can call the function from anywhere providing the desired action after fetching data from db is finished:
getValueFromDatabase(completion:{
self.collectionView.reloadData() //or any other action you want to fulfil
})

swift 3 firebase snapshot if value equal to ... fetch

I'm Trying to check if the rooms's value 'Owner' equals to the current user id if so then fetch all data including the key value and continue checking other children of 'rooms'
I was trying, but I fail finding the solution though it might seem easy so please help me with your suggestions or ideas. My code so far :
Database.database().reference().child("rooms").queryOrdered(byChild: "Owner").observeSingleEvent(of: .value, with: { (snapshot) in
let currentUser = Auth.auth().currentUser?.uid
if !snapshot.exists() {
print("No data found")
return
}
var rooms = snapshot.value as! [String:AnyObject]
let roomKeys = Array(rooms.keys)
for roomKey in roomKeys {
guard
let value = rooms[roomKey] as? [String:AnyObject]
else
{
continue
}
let title = value["title"] as? String
let description = value["description"] as? String
let roomPictureUrl = value["Room Picture"] as? String
let longitude = value["Longtitude"] as? String
let latitude = value["Latitude"] as? String
let dateFrom = value["Date From"] as? String
let dateTo = value["Date To"] as? String
let owner = value["Owner"] as? String
let myRooms = Room(roomID: roomKey,title: title!, description: description!, roomPicutreURL: roomPictureUrl!, longitude: longitude!, latitude: latitude!, dateFrom: dateFrom!, dateTo: dateTo!, owner: owner!)
self.rooms.append(myRooms)
self.tableView.reloadData()
print(snapshot.value)
}
})
You're missing the value in your query:
Database.database().reference()
.child("rooms")
.queryOrdered(byChild: "Owner")
.queryEqual(toValue: "STbz...")
.observeSingleEvent(of: .value, with: { (snapshot) in
See for this and more query operators, the documentation on filtering data.
Mark:- Swift 5
Database.database().reference().child("user")
.queryOrdered(byChild: "UserPhoneNumber") //in which column you want to find
.queryEqual(toValue: "Your phone number or any column value")
.observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.childrenCount > 0
{
if let snapShot = snapshot.children.allObjects as? [DataSnapshot] {
//MARK:- User Exist in database
for snap in snapShot{
//MARK:- User auto id for exist user
print(snap.key)
break
}
}
}
else if snapshot.childrenCount == 0
{
//MARK:- User not exist no data found
}
})

Trouble understanding array appending

func EmptySearchBarList(){
self.PersonalSearchesList = []
currentUserFirebaseReference.child("rooms").observeSingleEvent(of: .value) { (snapshot: FIRDataSnapshot) in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
print("snap")
print(self.PersonalSearchesList.count) // Calling 0 every single time
DataService.ds.REF_INTERESTS.child(interestKey).observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
if snapshot.value != nil {
if let users = (snapshot.value! as? NSDictionary)?["users"] as? Dictionary<String,AnyObject> {
DataService.ds.REF_USERS.child(topUser).child("pictureURL").observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
self.PersonalSearchesList.append(eachSearch)
print("first one")
print(self.PersonalSearchesList.count)
})
}
}
})
}
print("Second one")
print(self.PersonalSearchesList.count)
print("Calling to set my sorted PersonalSearchesList")
self.mySortedList = self.PersonalSearchesList.sorted{ $0.users > $1.users }
}
}
initialLoadOver = true
}
What code I'm trying to ultimately run is this :
var mySortedList = [Interest](){
didSet{
print("this was called")
let myCount = mySortedList.count
print(self.PersonalSearchesList.count)
print(myCount)
self.tableView.reloadData()
}
}
The attempt is to load up my PersonalSearchesList array, and once the snap in snapshots is done running, I'm setting MySortedList equal to PersonalSearchesList and reloading the tableview.
What I don't understand is why the prints are coming out like they are. The snap/ 0's are coming from the top of my for snap in snapshots. It seems like it should instead be snap / 1 , snap / 2, snap / 3.
The code to be called when the snaps are done is correct in the timeline, once the snaps have gone through that code runs. What doesn't make sense is why it's not until after that the items are actually being appended to PersonalSearchesList. Becuase of how everything is Im' setting my filtered array to an empty personal searches array and then afterwards I'm filling it up.
Any ideas here?
edit:
var dispatchGroup = DispatchGroup()
dispatchGroup.enter()
dispatchGroup.leave()
dispatchGroup.notify(queue: DispatchQueue.global(), execute: {
})
DataService.ds.REF_INTERESTS.child(interestKey).observeSingleEvent is running asynchronous so all of those callbacks (where you actually fill you list) will run when they are called from the DataService.
You could use a dispatch group to do what you want.
http://commandshift.co.uk/blog/2014/03/19/using-dispatch-groups-to-wait-for-multiple-web-services/
func EmptySearchBarList(){
var dispatchGroup = DispatchGroup()
self.PersonalSearchesList = []
currentUserFirebaseReference.child("rooms").observeSingleEvent(of: .value) { (snapshot: FIRDataSnapshot) in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
dispatchGroup.enter()
single time
DataService.ds.REF_INTERESTS.child(interestKey).observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
if snapshot.value != nil {
if let users = (snapshot.value! as? NSDictionary)?["users"] as? Dictionary<String,AnyObject> {
DataService.ds.REF_USERS.child(topUser).child("pictureURL").observeSingleEvent(of: .value, with: { (snapshot: FIRDataSnapshot) in
self.PersonalSearchesList.append(eachSearch)
print("snap")
print(self.PersonalSearchesList.count)
})
}
}
dispatchGroup.leave()
})
}
dispatchGroup.notify(queue: DispatchQueue.global(), execute: {
print("Second one")
print(self.PersonalSearchesList.count)
print("Calling to set my sorted PersonalSearchesList")
self.mySortedList = self.PersonalSearchesList.sorted{ $0.users > $1.users }
})
}
}
initialLoadOver = true
}
I'm not completely sure you can enter and leave the group several times from the same thread so you might have to wrap the code from enter until the end of the callback in another thread and call it async

Resources