I’m trying to modify code written a by a previous programmer. He wrote a getPostFromFirebase() function where it updates the tableview when 1) the app loads up due to it’s presence in viewDidLoad and 2) when there is a new post add by a user. The problem is that he used a .observe(.childAdded) event type which means when a post is deleted or modified, the tableView will not update(my end goal to do). When I change .childAdded to .value, the current data doesn’t get loaded upon launch. I’ve been banging my head against the wall to figure out a if let statement to add a .value event type so the view can refresh after any change(if that’s even possible). I’m familiar with Firebase RT DB hence how I was able to ID the observer issue but I’m no where near close to as good as I’d like to be so any help is appreciated.
func getPostFromFirebase() {
let mostRecent = dbRef.lastestPostsQuery(count: 10)
mostRecent.keepSynced(true)
mostRecent.observe(.childAdded) { (snapshot: DataSnapshot) in
/*parse method in the PostFetcher class
that returns the post data or an error by way of a tuple.*/
let (post, error) = PostFetcher.parsePostSnapshot(snapshot: snapshot)
if let post = post {
self.latestPosts.append(post)
if let postId = post.postId { print("PostId = \(postId)") }
}
if let error = error {
print("\(#function) - \(error)")
}
}
}
Edit:
Thanks to Franks help I was able to implement his suggestion and added a .removeAll() to remove the current state and have the view append a fresh snapshot. Whether a post is added or deleted, the view now updates as I'd like it to do.
func getPostFromFirebase() {
let mostRecent = dbRef.lastestPostsQuery(count: 10)
mostRecent.keepSynced(true)
mostRecent.observe(.value) { (snapshot: DataSnapshot) in
self.latestPosts.removeAll()
for child in snapshot.children.allObjects as! [DataSnapshot] {
let (post, error) = PostFetcher.parsePostSnapshot(snapshot: child)
if let post = post {
self.latestPosts.append(post)
self.tableView.reloadData()
if let postId = post.postId { print("PostId = \(postId)") }
}
if let error = error {
print("\(#function) - \(error)")
}
}
}
}
The .child* events fire on child nodes of the location/query that you observe, while .value fires on the location/query itself. This means that the value you get is one level higher up in the JSON, and you'll need to loop over the results:
mostRecent.observe(.value) { (snapshot: DataSnapshot) in
for child in snapshot.children.allObjects as! [DataSnapshot] {
let (post, error) = PostFetcher.parsePostSnapshot(snapshot: child)
if let post = post {
self.latestPosts.append(post)
if let postId = post.postId { print("PostId = \(postId)") }
}
if let error = error {
print("\(#function) - \(error)")
}
}
}
Alternatively, you can listen to the .childChanged and .childRemoved events (in addition to .childAdded that you already have) and handle them separately. This may be better in cases where you need to update the UI efficiently, since it allows you to handle each individual case (new node, changed, node, removed node) in the most efficient way.
Related
I have some code that reads data from Firebase on a custom loading screen that I only want to segue once all of the data in the collection has been read (I know beforehand that there won't be more than 10 or 15 data entries to read, and I'm checking to make sure the user has an internet connection). I have a loading animation I'd like to implement that is started by calling activityIndicatorView.startAnimating() and stopped by calling activityIndicatorView.stopAnimating(). I'm not sure where to place these or the perform segue function in relation to the data retrieval function. Any help is appreciated!
let db = Firestore.firestore()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else{
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
}
You don't need to know the progress of the read as such, just when it starts and when it is complete, so that you can start and stop your activity view.
The read starts when you call getDocuments.
The read is complete after the for loop in the getDocuments completion closure.
So:
let db = Firestore.firestore()
activityIndicatorView.startAnimating()
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else {
for doc in snapshot!.documents{
self.packageIDS.append(doc.documentID)
self.packageNames.append(doc.get("title") as! String)
self.packageIMGIDS.append(doc.get("imgID") as! String)
self.packageRadii.append(doc.get("radius") as! String)
}
}
DispatchQueue.main.async {
activityIndicatorView.stopAnimating()
}
}
As a matter of style, having multiple arrays with associate data is a bit of a code smell. Rather you should create a struct with the relevant properties and create a single array of instances of this struct.
You should also avoid force unwrapping.
struct PackageInfo {
let id: String
let name: String
let imageId: String
let radius: String
}
...
var packages:[PackageInfo] = []
...
db.collection("Packages").getDocuments{(snapshot, error) in
if error != nil{
// DB error
} else if let documents = snapshot?.documents {
self.packages = documents.compactMap { doc in
if let title = doc.get("title") as? String,
let imageId = doc.get("imgID") as? String,
let radius = doc.get("radius") as? String {
return PackageInfo(id: doc.documentID, name: title, imageId: imageId, radius: radius)
} else {
return nil
}
}
}
There is no progress reporting within a single read operation, either it's pending or it's completed.
If you want more granular reporting, you can implement pagination yourself so that you know how many items you've already read. If you want to show progress against the total, this means you will also need to track the total count yourself though.
I use the pagination method at the bottom of this question which works fine. Once the startKey is initialized with a key from the db that's the point at which the next pagination will occur from and the next set of posts (children) will get appended to the datasource.
I realized that if that key got deleted by the initial user who posted it, then once I try to paginate from that key since it doesn't exist the children that would get appended if it was there wouldn't get appended because they wouldn't be accessible (they're accessible based on that key).
The only thing I could think of was to first check if the key exists() and if it doesn't just start everything over from the beginning:
if !snapshot.exists() {
self?.startKey = nil
self?.datasource.removeAll()
self?.collectionView.reloadData()
self?.handlePagination()
return
}
It works fine but it's not the most fluid user experience because I'd rather just pull the posts prior to the deleted key (I have no prior reference to them).
A possibility is to just keep an array of all the previous keys and just loop through them but there's always a minute chance that those keys can get deleted by the users who posted them also.
Any ideas of how to get around this?
var startKey: String?
func handlePagination() {
if startKey == nil {
Database...Ref.child("posts")
.queryOrderedByKey()
.queryLimited(toLast: 7)
.observeSingleEvent(of: .value, with: { [weak self](snapshot) in
guard let children = snapshot.children.allObjects.first as? DataSnapshot else { return}
for child in snapshot.children.allObjects as! [DataSnapshot] {
// append child to datasource
}
self?.startKey = children.key
})
} else {
Database...Ref.child("posts")
.queryOrderedByKey()
.queryEnding(atValue: startKey!)
.queryLimited(toLast: 8)
.observeSingleEvent(of: .value, with: { [weak self](snapshot) in
if !snapshot.exists() {
self?.startKey = nil
self?.datasource.removeAll()
self?.collectionView.reloadData()
self?.handlePagination()
return
}
guard let children = snapshot.children.allObjects.first as? DataSnapshot else { return}
for child in snapshot.children.allObjects as! [DataSnapshot] {
// insert child in datasource at startIndex
}
self?.startKey = children.key
})
}
}
there's always a minute chance that those keys can get deleted by the users who posted them also
You're saying that you could keep the keys of the current page, and just loop through until you find an item that was not deleted. In the case that all have been deleted, you'd reach the end of the list of keys, and should create a new query that doesn't have a startAt(). This will give you the first X items, which is the correct behavior in that case I think.
In general though: dealing with realtime and pagination is really hard, which is the main reason the paging adapters in FirebaseUI don't do realtime updates.
I'm trying to append "the retrieved data -Keys- from firebase" into an array but it doesn't work
This is the for loop output #2 the retrieved keys
This the keys from firebase
This is the code
let ref = Database.database().reference()
ref.child("Faculty ").observe(.value, with: { (snapshot) in
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let FacultyName = child.key as! String
print(FacultyName)
self.NamesofFac.append(FacultyName)
}
}
})
for i in 0...self.NamesofFac.count {
print(self.NamesofFac.count)
print(" line")
print(self.NamesofFac)
The problem you are having is the Firebase Observe function give a callback in the form of a (snapshot).
It takes a bit of time to go to the web to get the data, therefore, firebase returns the data asynchronously. Therefore your code in your for loop will run before your firebase data has been returned. At the time your for loop code runs the array is still blank. But the for loop code in a separate function as you see in my sample code and call it straight after your for loop inside your firebase observe call.
Try this instead:
override func viewDidLoad() {
getFirebaseData()
}
func getFirebaseData() {
let ref = Database.database().reference()
ref.child("Faculty ").observe(.value, with: { (snapshot) in
if let result = snapshot.children.allObjects as? [DataSnapshot] {
for child in result {
let FacultyName = child.key as! String
print(FacultyName)
self.NamesofFac.append(FacultyName)
}
printNames()
}
})
}
func printNames() {
for i in 0...self.NamesofFac.count {
print(self.NamesofFac.count)
print(" line")
print(self.NamesofFac)
}
}
This was it won't print the names until they have been fully loaded from firebase.
PS: Your naming conventions are incorrect. You seem to be naming variables with a capital letter. Variables should be camel case. Classes should start with a capital.
I am making a completion handler for a function which will return a list of objects. When it return value for first time, it works well. But when any change happen into firebase database and again observe gets called, array size gets doubled up. Why it's getting doubled up?
func getStadiums(complition: #escaping ([Stadium]) -> Void){
var stadiums: [Stadium] = []
let stadiumRef = Database.database().reference().child("Stadium")
stadiumRef.observe(.value, with: { (snapshot) in
for snap in snapshot.children {
guard let stadiumSnap = snap as? DataSnapshot else {
print("Something wrong with Firebase DataSnapshot")
complition(stadiums)
return
}
let stadium = Stadium(snap: stadiumSnap)
stadiums.append(stadium)
}
complition(stadiums)
})
}
And calling like this
getStadiums(){ stadiums
print(stadiums.count) // count gets doubled up after every observe call
}
The code you're using declares stadiums outside of the observer. This means any time a change is made to the value of the database reference, you're appending the data onto stadiums without clearing what was there before. Make sure to remove the data from stadiums before appending the snapshots again:
func getStadiums(complition: #escaping ([Stadium]) -> Void){
var stadiums: [Stadium] = []
let stadiumRef = Database.database().reference().child("Stadium")
stadiumRef.observe(.value, with: { (snapshot) in
stadiums.removeAll() // start with an empty array
for snap in snapshot.children {
guard let stadiumSnap = snap as? DataSnapshot else {
print("Something wrong with Firebase DataSnapshot")
complition(stadiums)
return
}
let stadium = Stadium(snap: stadiumSnap)
stadiums.append(stadium)
}
complition(stadiums)
})
}
This line stadiumRef.observe(.value, with: { (snapshot) in ... actually adding an observer that will be called everytime your stadium data is changed.
Because you called it twice by using getStadiums(){ stadiums ..., the total observer added will be 2.
That makes the line stadiums.append(stadium) called twice in the second call.
My suggestion would be to use stadiumRef.observe() once without calling it from getStadiums().
Create a Model as below
class OrderListModel: NSObject {
var Order:String?
var Date:String?
}
Use the below code in the view controller and you should be able to see content in your tableview
func getOrdersData() {
self.orderListArr.removeAll()
let ref = Database.database().reference().child(“users”).child(user).child("Orders")
ref.observe(.childAdded, with: { (snapshot) in
print(snapshot)
guard let dictionary = snapshot.value as? [String : AnyObject] else {
return
}
let orderObj = OrderModel()
orderObj.Order = dictionary[“Order”] as? String
orderObj.Date = dictionary[“Date”] as? String
self.orderListArr.append(orderObj)
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.reloadData()
}, withCancel: nil)
}
func ListenForChildrenAdded() {
let registerToListenTo = "YourPathHere"
ref.child(registerToListenTo).observeSingleEvent(of: .value) { (snapshot) in
let initialChildren = snapshot.childrenCount
var incrementer = 0
ref.child(registerToListenTo).observe(.childAdded, with: { (snapshot) in
incrementer += 1
print("snapshot: \(snapshot.key) #\(incrementer)")
if incrementer == initialChildren {
print("-> All children found")
} else if incrementer > initialChildren {
print("-> Child Was Added - Run Some Code Here")
}
})
}}
I have an NSArray containing multiple ids. Is there a way in Firebase where I can get all the object with the ids in the array?
I am building a restaurant rating app which uses GeoFire to retrieve nearby restaurants. My problem is that GeoFire only returns a list of ids of restaurant that are nearby. Is there any way i can query for all the object with the ids?
No, you can't do a batch query like that in Firebase.
You will need to loop over your restaurant IDs and query each one using observeSingleEvent. For instance:
let restaurantIDs: NSArray = ...
let db = FIRDatabase.database().reference()
for id in restaurantIDs as! [String] {
db.child("Restaurants").child(id).observeSingleEvent(of: .value) {
(snapshot) in
let restaurant = snapshot.value as! [String: Any]
// Process restaurant...
}
}
If you are worried about performance, Firebase might be able to group all these observeSingleEvent calls and send them as a batch to the server, which may answer your original question after all ;-)
I know that this answer is considered accepted but I have had really good success using promise kit with the method frank posted with his javascript link Speed up fetching posts for my social network app by using query instead of observing a single event repeatedly and just wanted to share the swift version
So I have a list of users ids that are attached to a post like this:
also these methods are in my post class where I have access to the post id from firebase
// this gets the list of ids for the users to fetch ["userid1", "userid2"....]
func getParticipantsIds() -> Promise<[String]> {
return Promise { response in
let participants = ref?.child(self.key!).child("people")
participants?.observeSingleEvent(of: .value, with: { (snapshot) in
guard let snapshotIds = snapshot.value as? [String] else {
response.reject(FirebaseError.noData)
return
}
response.fulfill(snapshotIds)
})
}
}
// this is the individual query to fetch the userid
private func getUserById(id:String) -> Promise<UserData> {
return Promise { response in
let userById = dbRef?.child("users").child(id)
userById?.observeSingleEvent(of: .value, with: { (snapshot) in
guard let value = snapshot.value else {
response.reject(FirebaseError.noData)
return
}
do {
let userData = try FirebaseDecoder().decode(UserData.self, from: value)
response.fulfill(userData)
} catch let error {
response.reject(error)
}
})
}
}
// this is the where the magic happens
func getPostUsers(compeltion: #escaping (_ users:[UserData], _ error:Error?) -> ()){
getParticipantsIds().thenMap { (id) in
return self.getUserById(id: id)
}.done { (users) in
compeltion(users, nil)
}.catch({ error in
compeltion([], error)
})
}