Displaying all posts using Firebase in Swift? - swift

My goal is so that when users open the app they will see every post from every user of the app. My data tree is as follows:
- posts
- UID
- postKey
--attributes
- users
- UID
- attributes
Here's an example:
-posts
-74anqEXU8kQHVr7IKoO3N9NNqDh1
-MLzvs5VvXB_z7fhbh2p
created_at: 1605242945.969368
image_height: 414.6865671641791
image_url: "https://firebasestorage...."
-MLzvun01fNXNRbn7TPv
-MM7zGyZ7GbenhlisJUZ
-VGxqdzc2CkWWn39pa8xUofEWgNm2
-pNvGg84JR0TbXvS4XlX8KbfBasz2
-users
-74anqEXU8kQHVr7IKoO3N9NNqDh1
-VGxqdzc2CkWWn39pa8xUofEWgNm2
I know I have to make a snapshot of all the posts, and then display it in viewDidLoad() of the main controller (SearchViewController). I can't figure out how to display all posts.
This code works for displaying the Current User's posts, stored in a file called UserService.swift:
static func posts(for user: User, completion: #escaping ([Post]) -> Void) {
let ref = Database.database().reference().child("posts").child(user.uid)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let snapshot = snapshot.children.allObjects as? [DataSnapshot] else {
return completion([])
}
let dispatchGroup = DispatchGroup()
let posts: [Post] = snapshot.reversed().compactMap {
guard let post = Post(snapshot: $0) else { return nil }
dispatchGroup.enter()
SaveService.isPostSaved(post) { (isSaved) in
post.isSaved = isSaved
dispatchGroup.leave()
}
return post
}
dispatchGroup.notify(queue: .main, execute: {
completion(posts)
})
})
}
And then in ViewDidLoad() of SearchViewController:
UserService.posts(for: User.current) { (posts) in
self.posts = posts
self.tableView.reloadData()
}
But how do I do this for all posts. When I try the following, it won't let me specify a random user:
let ref = Database.database().reference().child("posts").child(User)
Any idea what I can put in the ".child(User" box to make this work? Or how to create a for-loop to properly iterate through?
*EDIT
Here is what worked:
static func allPosts(completion: #escaping ([Post]) -> Void) {
let ref = Database.database().reference().child("posts")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
var postsArray = [Post]()
for userSnapshot in snapshot.children {
guard let snapshot = (userSnapshot as AnyObject).children.allObjects as? [DataSnapshot] else {
return completion([])
}
let dispatchGroup = DispatchGroup()
let posts: [Post] = snapshot.reversed().compactMap {
guard let post = Post(snapshot: $0) else { return nil }
postsArray
postsArray.append(post)
dispatchGroup.enter()
SaveService.isPostSaved(post) { (isSaved) in
post.isSaved = isSaved
dispatchGroup.leave()
}
return post
}
dispatchGroup.notify(queue: .main, execute: {
completion(postsArray)
})
}
})
}

I may be overlooking something but it appears the goal is to
when users open the app they will see every post from every user
If that's the case, sometimes simpler is better; load all of the posts, iterate over them creating your Post object for each then reload the tableview (?)
let postsRef = self.ref.child("posts") //self.ref points to my firebase
postsRef.observeSingleEvent(of: .value, with: { snapshot in
let allPosts = snapshot.children.allObjects as! [DataSnapshot]
for postSnap in allPosts {
let aPost = Post(initWithSnapshot: postSnap)
self.postsArrray.append(post) //
}
self.postTableView.reloadData()
}
This assumes your Post class has an convenience init to populate its properties from the snapshot. If so, set isSaved to true within that since the convenience init is being called with saved data.
If you're calling this when the app starts or view loads, there's no need for callbacks and dispatchQueues.

You can observe one level higher in the JSON, and then add an extra loop in the callback:
let ref = Database.database().reference().child("posts")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
for userSnapshot in snapshot.children {
guard let snapshot = userSnapshot.children.allObjects as? [DataSnapshot] else {
return completion([])
}
let dispatchGroup = DispatchGroup()
let posts: [Post] = snapshot.reversed().compactMap {
guard let post = Post(snapshot: $0) else { return nil }
dispatchGroup.enter()
SaveService.isPostSaved(post) { (isSaved) in
post.isSaved = isSaved
dispatchGroup.leave()
}
return post
}
}
dispatchGroup.notify(queue: .main, execute: {
completion(posts)
})
})

Related

How to know when Firebase has finished downloading all the nodes in a snapshot using Swift?

I have the following function which fetches messages from Firebase, I want to know when the Firebase has finished loading all child nodes of the data snapshot so I can pass back a signal via handler that all the data has finished loading.
func loadLatestChatConversationMessages(forUserId forId: String!, handler: #escaping (Message?) -> ()){
guard let uid = Auth.auth().currentUser?.uid else {return}
let ref = REF_USERS_MESSAGES.child(uid).child(forId)
let firstBatchCount: Int = 12
RefUserMessagesChatConvoHandle = ref.queryOrderedByKey().queryLimited(toLast: UInt(firstBatchCount)).observe(.value, with: { (snapshot) in
if snapshot.childrenCount > 0 {
for child in snapshot.children.allObjects as! [DataSnapshot]{
let key = child.key
self.REF_MESSAGES.child(key).observeSingleEvent(of: .value, with: { (snapshot) in
guard var dictionary = snapshot.value as? [String : Any] else { return }
dictionary["messageId"] = key
let message = Message(dictionary: dictionary)
handler(message)
}, withCancel: nil)
}//end for
} else {
print("returning - no messages.")
handler(nil)
}// end if snapshot.childrenCount
}, withCancel: nil)
}//end func
on the top create a dispatchGroup object and enter() right away.
https://developer.apple.com/documentation/dispatch/dispatchgroup
let dg = DispatchGroup()
and then in each child download you want to do you enter() and leave() once its done.
then finally you call
dg.notify(queue: .main, work: {
// escape the close
})

How to load datas not in realtime in firebase?

I have the following problem to solve:
All datas are loaded in realtime (this time even multiple times per post), but I just want to refresh with a refresher I already have.
This is my refresher:
// Refresher
func refresh() {
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Aktualisieren")
refresher.addTarget(self, action: #selector(DiscoveryViewController.refreshData) , for: UIControl.Event.valueChanged)
tableView.addSubview(refresher)
}
#objc func refreshData(sender: Any) {
loadTopPosts()
refresher.endRefreshing()
}
And with this I load all posts:
func loadTopPosts() {
ProgressHUD.show("Lade...", interaction: false)
self.postArray.removeAll()
self.tableView.reloadData()
// Aktuelle Location des aktuell eingeloggten Users laden
guard let currentUserUid = UserApi.shared.CURRENT_USER_ID else { return }
let databaseRef = LocationApi.shared.geoRef
databaseRef.getLocationForKey(currentUserUid) { (location, error) in
if error != nil {
ProgressHUD.showError("Posts konnten nicht geladen werden")
} else if location != nil {
print("Location for \(currentUserUid) is [\(location!.coordinate.latitude), \(location!.coordinate.longitude)]")
// Alle Posts im vorgegebenen Umkreis laden
let REF_GEO_POSTS = Database.database().reference().child("geolocation_posts")
let geoRef = GeoFire(firebaseRef: REF_GEO_POSTS)
// Lade den aktuell eingestellten Radius aus der Datenbank
self.observeRadius(completion: { (radius) in
let currentRadius = radius
// Üperprüfe, welche Posts im Umkreis erstellt wurden
let circleQuery = geoRef.query(at: location!, withRadius: Double(currentRadius)!)
circleQuery.observe(.keyEntered, with: { (postIds, location) in
self.observePost(withPostId: postIds, completion: { (posts) in
guard let userUid = posts.uid else { return }
self.observeUser(uid: userUid, completion: { (users) in
let postArray = UserPostModel(post: posts, user: users)
self.postArray.append(postArray)
self.postArray.sort(by: {$0.post!.secondsFrom1970! > $1.post!.secondsFrom1970!})
self.tableView.reloadData()
self.tableView.setContentOffset(CGPoint.zero, animated: true)
ProgressHUD.dismiss()
})
})
})
})
if self.postArray.count == 0 {
ProgressHUD.dismiss()
}
} else {
ProgressHUD.showError("Posts konnten nicht geladen werden")
}
}
}
Here are the functions where I over serve datas from firebase:
let REF_POSTS = Database.database().reference().child("posts")
func observePost(withPostId id: String, completion: #escaping (PostModel) -> Void) {
REF_POSTS.child(id).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newPost = PostModel(dictionary: dic, key: snapshot.key)
completion(newPost)
}
}
let REF_USERS = Database.database().reference().child("users")
func observeUser(uid: String, completion: #escaping (UserModel) -> Void) {
REF_USERS.child(uid).observeSingleEvent(of: .value) { (snapshot) in
guard let dic = snapshot.value as? [String: Any] else { return }
let newUser = UserModel(dictionary: dic)
completion(newUser)
}
}
func observeRadius(completion: #escaping (String) -> Void) {
guard let currentUserUid = UserApi.shared.CURRENT_USER_ID else { return }
let REF_RADIUS = Database.database().reference().child("users").child(currentUserUid).child("radius")
REF_RADIUS.observeSingleEvent(of: .value) { (radius) in
let currentRadius = radius.value as? String
completion(currentRadius!)
}
}
What I now want to do is to disable the realtime function (updating the tableView only if I refresh). So if I refresh, everything will be displayed correctly.
How to solve this problem?
Thanks in advance for your help!
There is observe, that keeps notifying every time an update happens in the database. And there is observeSingleEvent, that will only provide you data when requested.
https://firebase.google.com/docs/database/ios/read-and-write
And also, your geofire reference will keep notifying you for every update, if you don't want it to do that, remove it like this:
"If you're not interested in getting updates on new/moving users after the initial query, this is also a great moment to remove your observer by calling removeObserverWithFirebaseHandle or removeAllObservers."
in your case it's posts, and that was mentioned here:
https://stackoverflow.com/a/50722984/8869493

Posts being loaded for all users in collection view firebase & swift

All I'm trying to do is fetch all the posts to the collection view that are within a certain radius of the current user. Currently I'm getting all the current users posts within the location but that is all. I can't figure out how to convert it to fetch all the posts from all the users.
FetchPostUserIds Is returning a snapshot of all the users and there UID
The geoFire query is only returning the postId from the current user. It shouldn't be I assume
Note: Updated Code
var PostKey: String?
var geoFire: GeoFire?
var regionQuery: GFRegionQuery?
var foundQuery: GFCircleQuery?
var geoFireRef: DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
guard let uid = Auth.auth().currentUser?.uid else { return }
geoFireRef = Database.database().reference().child("posts").child(uid)
geoFire = GeoFire(firebaseRef: geoFireRef)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let userLocation: CLLocation = locations[0] as CLLocation
let currentUserLocation = userLocation
let circleQuery = geoFire?.query(at: currentUserLocation, withRadius: 100.0)
_ = circleQuery?.observe(.keyEntered, with: { (key, location) in
self.PostKey = key
self.locationManager.stopUpdatingLocation()
})
}
fileprivate func fetchPostsWithUser(user: User) {
guard let key = PostKey else { return }
let ref = Database.database().reference().child("posts").child(user.uid).child(key)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
self.collectionView?.refreshControl?.endRefreshing()
guard let dictionary = snapshot.value as? [String: Any] else { return }
var post = Post(user: user, dictionary: dictionary)
post.id = key
self.posts.append(post)
self.posts.sort(by: { (post1, post2) -> Bool in
return post1.creationDate.compare(post2.creationDate) == .orderedDescending
})
self.collectionView?.reloadData()
}) { (error) in
print(error)
}
}
fileprivate func fetchPostUserIds() {
let ref = Database.database().reference().child("users")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let userIdKey = snapshot.value as? [String: Any] else { return }
userIdKey.forEach({ (key, value) in
Database.fetchUserWithUID(uid: key, completion: { (user) in
self.fetchPostsWithUser(user: user)
})
})
}) { (error) in
print(error)
}
}
Try to debug and look what you got in the snapshot in your function, and also what fetchUserWithUID return
fileprivate func fetchPostUserIds() {
let ref = Database.database().reference().child("users")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let userIdKey = snapshot.value as? [String: Any] else { return }
userIdKey.forEach({ (key, value) in
Database.fetchUserWithUID(uid: key, completion: { (user) in
self.fetchPostsWithUser(user: user)
})
})
}) { (error) in
print(error)
}
}
Maybe with some more information I can help you
You pass paremeter user: User to method fetchPostsWithUser, but you always use the current user
guard let uid = Auth.auth().currentUser?.uid else { return }
also note these
let ref = Database.database().reference().child("users")
ref.observe(.value, with: { (snapshot) in
will load every change so think about singleObserve

How to wait for Swift's URLSession to finish before running again?

Probably a stupid question, but I'm a beginner at this.
The below code is supposed to get book information from Google Books from a keyword search. It then goes through the results and checks if I have a matching ISBN in a Firebase database. It works, but currently can only search 40 books as that's the Google Books API maximum per search.
Fortunately, I can specify where to start the index and get the next 40 books to search as well. Unfortunately, I've been trying for hours to understand how the URLSession works. All the methods I've tried have shown me that the code after the URLSession block doesn't necessarily wait for the session to complete. So if I check if I've found any matches afterward, it might not even be done searching.
I suspect the answer is in completion handling, but my attempts so far have been unsuccessful. Below is my code with a URL setup to take various starting index values.
var startingIndex = 0
//encode keyword(s) to be appended to URL
let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = "https://www.googleapis.com/books/v1/volumes?q=\(query)&&maxResults=40&startIndex=\(startingIndex)"
URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}else{
let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String: AnyObject]
if let items = json["items"] as? [[String: AnyObject]] {
//for each result make a book and add title
for item in items {
if let volumeInfo = item["volumeInfo"] as? [String: AnyObject] {
let book = Book()
//default values
book.isbn13 = "isbn13"
book.isbn10 = "isbn10"
book.title = volumeInfo["title"] as? String
//putting all authors into one string
if let temp = volumeInfo["authors"] as? [String] {
var authors = ""
for i in 0..<temp.count {
authors = authors + temp[i]
}
book.author = authors
}
if let imageLinks = volumeInfo["imageLinks"] as? [String: String] {
book.imageURL = imageLinks["thumbnail"]
}
//assign isbns
if let isbns = volumeInfo["industryIdentifiers"] as? [[String: String]] {
for i in 0..<isbns.count {
let firstIsbn = isbns[i]
if firstIsbn["type"] == "ISBN_10" {
book.isbn10 = firstIsbn["identifier"]
}else{
book.isbn13 = firstIsbn["identifier"]
}
}
}
//adding book to an array of books
myDatabase.child("listings").child(book.isbn13!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
}
DispatchQueue.main.async { self.tableView.reloadData() }
}
})
myDatabase.child("listings").child(book.isbn10!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
}
DispatchQueue.main.async { self.tableView.reloadData() }
}
})
}
}
}
}
SVProgressHUD.dismiss()
}.resume()
Below is my revised code:
func searchForSale(query: String, startingIndex: Int) {
directionsTextLabel.isHidden = true
tableView.isHidden = false
listings.removeAll()
DispatchQueue.main.async { self.tableView.reloadData() }
SVProgressHUD.show(withStatus: "Searching")
//clear previous caches of textbook images
cache.clearMemoryCache()
cache.clearDiskCache()
cache.cleanExpiredDiskCache()
let url = "https://www.googleapis.com/books/v1/volumes?q=\(query)&&maxResults=40&startIndex=\(startingIndex)"
URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in
if error != nil {
print(error!.localizedDescription)
}else{
var needToContinueSearch = true
let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! [String: AnyObject]
if json["error"] == nil {
let totalItems = json["totalItems"] as? Int
if totalItems == 0 {
SVProgressHUD.showError(withStatus: "No matches found")
return
}
if let items = json["items"] as? [[String: AnyObject]] {
//for each result make a book and add title
for item in items {
if let volumeInfo = item["volumeInfo"] as? [String: AnyObject] {
let book = Book()
//default values
book.isbn13 = "isbn13"
book.isbn10 = "isbn10"
book.title = volumeInfo["title"] as? String
//putting all authors into one string
if let temp = volumeInfo["authors"] as? [String] {
var authors = ""
for i in 0..<temp.count {
authors = authors + temp[i]
}
book.author = authors
}
if let imageLinks = volumeInfo["imageLinks"] as? [String: String] {
book.imageURL = imageLinks["thumbnail"]
}
//assign isbns
if let isbns = volumeInfo["industryIdentifiers"] as? [[String: String]] {
for i in 0..<isbns.count {
let firstIsbn = isbns[i]
//checks if isbns have invalid characters
let isImproperlyFormatted = firstIsbn["identifier"]!.contains {".$#[]/".contains($0)}
if isImproperlyFormatted == false {
if firstIsbn["type"] == "ISBN_10" {
book.isbn10 = firstIsbn["identifier"]
}else{
book.isbn13 = firstIsbn["identifier"]
}
}
}
}
//adding book to an array of books
myDatabase.child("listings").child(book.isbn13!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
needToContinueSearch = false
}
DispatchQueue.main.async { self.tableView.reloadData() }
}
})
myDatabase.child("listings").child(book.isbn10!).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
if listings.contains(book) == false{
listings.append(book)
needToContinueSearch = false
}
DispatchQueue.main.async { self.tableView.reloadData() }
return
}
if startingIndex < 500 {
if needToContinueSearch {
let nextIndex = startingIndex + 40
self.searchForSale(query: query, startingIndex: nextIndex)
}
}
})
}
}
}
}else{
return
}
}
SVProgressHUD.dismiss()
}.resume()
//hide keyboard
self.searchBar.endEditing(true)
}
In your completion handler if any results have been returned you end with:
DispatchQueue.main.async { self.tableView.reloadData() }
to trigger reloading of your table with the updated information. At this same point is where you could determine of there may be more results and initiate the next asynchronous URL task. In outline your code might be:
let needToContinueSearch : Bool = ...;
DispatchQueue.main.async { self.tableView.reloadData() }
if needToContinueSearch
{ // call routine it initiate next async URL task
}
(If there is any reason to start the task from the main thread the if would be in the block.)
By not initiating the next search until after you've processed the results of the first you avoid having to deal with any issues of a subsequent callback trying to update your data at the same time as a previous one.
However if you find delaying the second search in this way is too slow you can investigate ways to overlap the operations, e.g. you might have the callback just pass the processing of the results to an async task on a serial queue (so that only one set of results is being processed at once) and initiate the next async URL task.
HTH
Declare a bool variable as isLoading and if that function is loading dont trigger urlsession. hope below sample will help you.
var isLoading : Bool = false
func loadMore(with pageCount: Int){
if isLoading { return }
isLoading = true
// call the network
URLSession.shared.dataTask(with: URL(string: "xxxxx")!) { (data, response, error) in
// after updating the data set isloding to false again
// do the api logic here
//
DispatchQueue.main.async {
// self.items = downloadedItems
self.tableView.reloadData()
self.isLoading = false
}
}.resume()
}

Firebase - Swift - Delete a child UID from a snapshot

I am trying to delete up the studentUser UID from my staffUsers. The UID I want to delete is nested in the staffUsers -> studentSession1List.
I have the UID listed with A Bool of True on creation. This "studentSession1List" will have lots of studentUsers in the list. I only want the studentUser that is logged in to have their UID(55FDLm9n6LccZBB7skaCbvfSHRz1) removed from the list.
let dataref = Database.database().reference()
dataref.child("staffUsers").queryOrdered(byChild: "studentSession1List").observe(.value, with: { (snapshot) in
for snap in snapshot.children {
guard let studentUID = Auth.auth().currentUser?.uid else { return }
let snapDataSnapshot = snap as! DataSnapshot
var snapValues = snapDataSnapshot.value as? [String: AnyObject]
if var snapWithReg = snapValues?["studentSession1List"] as? [String: Bool] {
print("This is the staff member")
print(snapWithReg)
print(snapWithReg.count)
snapWithReg.removeValue(forKey: studentUID)
}
}
}) { (error) in
print(error.localizedDescription)
}
Here is the output:
Full Function for Deleting and Adding the Student
func didSelect(for cell: StudentSearchCell) {
guard let indexpath = collectionView?.indexPath(for: cell) else { return }
let staffUser = self.users[indexpath.item]
let selectedUserId = staffUser.uid
guard let studentUID = Auth.auth().currentUser?.uid else { return }
let dataRef = Database.database().reference()
dataRef.child("staffUsers").queryOrdered(byChild: "studentSession1List").observe(.value, with: { (snapshot) in
for snap in snapshot.children {
guard let studentUID = Auth.auth().currentUser?.uid else { return }
let snapDataSnapshot = snap as! DataSnapshot
var snapValues = snapDataSnapshot.value as? [String: AnyObject]
if (snapValues? ["studentSession1List"] as? [String: Bool]) != nil {
dataRef.child("staffUsers").child(snapDataSnapshot.key).child("studentSession1List").child(studentUID).removeValue(completionBlock: { (error, ref) in
if error != nil {
print("Error: \(String(describing: error))")
return
}
print("Removed successfully")
})
}
}
}) { (error) in
print(error.localizedDescription)
}
// Add student to staff list
let ref = Database.database().reference().child("staffUsers").child(selectedUserId).child("studentSession1List")
let values = [studentUID: true]
ref.updateChildValues(values) { (err, ref) in
if let err = err {
print("Failed to follow user:", err)
return
}
}
// Add selected staff to student list
let studentRef = Database.database().reference().child("studentUsers").child(studentUID).child("studentSession1List")
studentRef.removeValue()
let studentValues = [selectedUserId: true]
studentRef.updateChildValues(studentValues) { (err, studentRef) in
if let err = err {
print("Failed to follow user:", err)
return
}
}
self.navigationController?.popViewController(animated: true)
}
I think you need to reach the child that you want to remove using the following code and then remove it.
Edit1:
Since inside staffUsers we have keys inside which studentSession1List is present inside which the value (studentUID) is present that we want to remove, so inside your already written code I have added the new code, please check
let dataref = Database.database().reference()
dataref.child("staffUsers").queryOrdered(byChild: "studentSession1List").observe(.value, with: { (snapshot) in
for snap in snapshot.children {
guard let studentUID = Auth.auth().currentUser?.uid else { return }
let snapDataSnapshot = snap as! DataSnapshot
var snapValues = snapDataSnapshot.value as? [String: AnyObject]
if var snapWithReg = snapValues?["studentSession1List"] as? [String: Bool] {
//Added code here
dataref.child("staffUsers").child(snapDataSnapshot.key).child("studentSession1List").child(studentUID).removeValue(completionBlock: { (error, ref) in
if error != nil {
print("Error: \(error)")
return
}
print("Removed successfully")
})
}
}
}) { (error) in
print(error.localizedDescription)
}
Edit2:
To delete the code once , we can use observeSingleEvent
observeSingleEvent(of: .value, with: { (snapshot) in
}, withCancel: nil)