-1

Okay, after some comments for more information to this question, let's try it:

In a viewController I'm calling a function loadPosts(). In this function I'm calling api.shared.function observePosts(). observePosts() downloads all posts at a specified database reference. with the first downloaded post back in the loadPosts() I'm calling self.fetchUser(uid: post.uid) in VC. This call api.shared.function observeUser() which gives me the user from the current post downloaded.

In observeUser() I'm filling users-array with user-object from type UserModel and in completion-block of loadPosts() I'm filling posts-array with post-object from type PostModel. After insert the post to posts-array at:0 I reload the tableView and get a view of all downloaded posts with matching info about the user that created it. (code comes after description)

Means: If there are 5 posts from type PostModel - there must be 5 userModel's, too. Every function is called 5 times - in this example with 5. Every function is running 5 times. (Tested with a print("call loadPosts")).

What I finally want is: Get the posts and the users - fill the arrays (when all posts at this time) are loaded. Make this step again - with posts2 and users2. If all four arrays are filled - an algorithm get them together in one posts-array and one users-array, like: post[0], post1, post[2], post2[0], post[3], post[4], post[5], post21 ... Of course the users will be putting together in one as well. THIS PART IS NO PROBLEM! I HAVE A WORKING CODE ... with hardcoded strings in the arrays

My problem is: When I'm calling a function at the end of a function it can be fact that the asynchronous task of loading the posts ( users ) is not done. So, how can I make the code "wait to complete"? I have tried this from StackOverflow and also tried to work with DispatchQueue but that don't work, too.

Before the code is coming - here is my workaround: xCode 11.3.1 with Swift 5. ( async / await - solutions from the comments are not possible )

Code(s): In ViewController:

viewDidLoad => loadPosts()

loadPosts(){
  Api.shared.observePosts { (post) in
    self.fetchUser(uid: UserUid, completed:{
      self.posts.insert(post, at:0)
      self.tableView.reloadData()
    })
  }
}
func fetchUser(uid: String, completed: @escaping () -> Void) {
      Api.shared.observeUser(uid: uid) { (user) in
          self.users.insert(user, at: 0)
          completed()
      } 
  } 

In Api

// load posts
  func observePosts(completion: @escaping (PostModel) -> Void){
      REF_POSTS.observe(.childAdded) { (snapshot) in
          guard let dic = snapshot.value as? [String: Any] else { return }
          let newPost = PostModel(dictionary: dic, key: snapshot.key)
          completion(newPost)
      } 
  } 
// load 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)
      } 
  }

I hope that now is clear what I'm doing and what I want to to after modifying the code. Any help or info is welcome.

Jason Aller
  • 3,475
  • 28
  • 40
  • 37
  • 1
    You should probably include your DispatchGroup attempt if you want to debug it. You could also try Swift's new async/await, which makes some of this easier. – jnpdx May 25 '22 at 15:39
  • When you say "I want **all** objects to be loaded": if you are observing `.childAdded`, how do you know when you have all objects? If posts can be added while you are downloading, how do you know which one is the last? In my case, I have batched the updates to the UI, so it happens once every 0.5 seconds. – HunterLion May 25 '22 at 16:10
  • You can easily convert this type of async code to use async await https://developer.apple.com/wwdc21/10132 – lorem ipsum May 25 '22 at 16:14
  • No i can't because I´m working with Xcode 11.3.1 and Swift 5 - async / await is available from Swift 5.5 @lorem ipsum – Steven Woodlink May 26 '22 at 03:44
  • No i can't because I´m working with Xcode 11.3.1 and Swift 5 - async / await is available from Swift 5.5 @jnpdx My attempts with DispatchGroup was not saved - so i can't show them. I tried so many versions & constellations - to reconstruct all of them i need to write a book ;-) – Steven Woodlink May 26 '22 at 03:48
  • I understand what you mean, @HunterLion but I don't understand what this info is important for me? I think we're talking about different requirements!? – Steven Woodlink May 26 '22 at 03:51
  • SO isn’t a code writing service. It sounds like you are asking for someone to write the code for you. There are other websites for that. We can’t help you troubleshoot if we don’t know where your issue is. – lorem ipsum May 26 '22 at 09:26
  • @loremipsum because i don‘t have the version 5.5 in XCode 11.3.1?! Cool ✌ – Steven Woodlink May 26 '22 at 10:54
  • My previous comment applies to any solution regardless of the Swift or Xcode version. Maybe you should look into some intro tutorials, you are likely missing the basics, they should be very helpful. – lorem ipsum May 26 '22 at 12:06
  • Thats not fact - you wrote „You can easily convert this type of async code to use async await developer.apple.com/wwdc21/10132“ and i said it is not possible for me with my workaround. And then you said i want others to write my code… not more and less - trust me i tried writing async ;-) @loremipsum – Steven Woodlink May 26 '22 at 12:36
  • That was the first comment. Before you shared a limitation. You haven't shown what its working about `DispatchGroup` you are just asking for a solution. SO is not the place to have someone write it for you. Without a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) it is impossible to help you troubleshoot. – lorem ipsum May 26 '22 at 12:52
  • Here's the issue; the code in the question doesn't give us a good picture of what you're attempting to do - it needs to be a [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). Secondly the description of what you're doing is vague; we don't know what a `PostModel` is or does, and the question says `If several objects of the PostModel type come out` - what does *come out* mean? Lastly, what does Firebase have to do with that? If you can update and clarify the question, we can probably help. – Jay May 26 '22 at 19:07
  • @Jay i have updated the post- hope i described full what happend – Steven Woodlink May 27 '22 at 13:18
  • @loremipsum i have updated the post- hope i described full what happend – Steven Woodlink May 27 '22 at 13:18
  • @jnpdx i have updated the post- hope i described full what happend – Steven Woodlink May 27 '22 at 13:19
  • I am probably overlooking something so forgive my questions: 1) This code `self.fetchUser(uid: UserUid` calls fetchUser over and over passing it the same UserUid because it's never updated, why do that? 2) `UserUid` appears to be a class or structure name because it's capitalized. If its a var is should be first char lower cased 3)The code inside `fetchUser` does this `self.posts.insert(post, at:0)` but inserting into posts is totally unrelated to fetching the user so why is it there? – Jay May 27 '22 at 17:31
  • 4) With 1000 posts, that means the tableView will be reloaded 1000 times and that's going to flicker and be a really bad user experience. Again, if I am overlooking something my apologies. It seems like you could just load the post and load the user put that data into a structure and stuff it into a single array. About 20 lines of code total. Let me try an answer. – Jay May 27 '22 at 18:11

1 Answers1

0

The question is a bit vague but what I think is being asked is:

I have two Firebase nodes (collections in Firestore), a post node and a user node. The post node contains child nodes that have a node that stores the uid of the user which are found in the users node. I want to load all the posts and the user associated with each post

If that's the question, here's a simplified solution:

First a structure to store the post and users name

struct PostAndUserStruct {
    var post = ""
    var userName = ""
}

and then a class var to use as a tableView dataSource

var postsAndUsersArray = [PostAndUserStruct]()

and then the code to read it in and populate that datasource for use with the tableView

func readPostsAndUser( completion: @escaping () -> Void ) {
    let postsRef = self.ref.child("posts")
    let usersRef = self.ref.child("users")

    postsRef.observeSingleEvent(of: .value, with: { snapshot in
        let allPosts = snapshot.children.allObjects as! [DataSnapshot]
        let lastPostIndex = allPosts.count - 1
        for (index, post) in allPosts.enumerated() {
            let postText = post.childSnapshot(forPath: "post_msg").value as? String ?? "No Msg"
            let uid = post.childSnapshot(forPath: "post_uid").value as? String ?? "No User"
            let thisUser = usersRef.child(uid)
            thisUser.observeSingleEvent(of: .value, with: { userSnapshot in
                let name = userSnapshot.childSnapshot(forPath: "name").value as? String ?? "No Name"
                let postAndUser = PostAndUserStruct(post: postText, userName: name)
                self.postsAndUsersArray.append(postAndUser)
                if index == lastPostIndex {
                    completion()
                }
            })
        }
    })
}

The flow code flow is

The postsAndUsersArray would be a tableView datasource.

Load all of the posts and cast the snapshot to an array to preserve ordering. Keep track of how many there are

Iterate over each post, capturing the post msg as well as the uid of the user that made the post.

Read each user, instantiate a PostAndUserStruct and add it to the class array (the tableViewDatasource)

When the loop gets to the last index, call the completion handler and output the results to console (good place to reload the tableView)

and is called like this

func handleButton0Action() {
    self.readPostsAndUser(completion: {
        for post in self.postsAndUsersArray {
            print(post.userName, post.post)
        }
        //this is a good place to reload the tableview
    })
}

The "note to self" on this is it's not super scaleable with large datasets but gets the job done for small dataset. If you have a millions posts you will need to implement pagination to read a chunk of posts at a time, otherwise you'll overload the device's memory and have random crashes.

NOTE: BELOW IS FIRESTORE CODE

The code to read the posts, read in the user for each post and return a populated array of PostAndUserStruct objects, noting self.db points to MY Firestore.

func readPostsAndUsersAsync() async -> [PostAndUserStruct] {
    let postsCollection = self.db.collection("posts")
    let usersCollection = self.db.collection("users")
    var postDataArray = [PostAndUserStruct]()

    let postSnapshot = try! await postsCollection.getDocuments()
    let allPosts = postSnapshot.documents

    for post in allPosts {
        let postText = post.get("post_msg") as? String ?? "No Post Message"
        if let uid = post.get("post_uid") as? String {
            let userDoc = usersCollection.document(uid)
            let userSnap = try! await userDoc.getDocument()
            let name = userSnap.get("name") as? String ?? "No Name"
            let postData = PostAndUserStruct(post: postText, userName: name)
            postDataArray.append(postData)
        }
    }

    return postDataArray
}

The code flow:

All of the documents are read from Firestore with getDocuments. Then we cast those documents to the allPosts array.

Iterate over the array, capturing the post_msg field from each post as well as the uid of the use that posted it.

Then, using the uid, read the users document from the users collection and get the users name.

Instantiate a PostAndUserStruct object with the post and users name and add it to an array.

Then return the populated array

That function is called like this and outputs the users name and post text to console.

func fetchPostsAndUsers() {
    Task {
        let postResults = await self.readPostsAndUsersAsync()
        for postAndUser in postResults {
            print(postAndUser.userName, postAndUser.post)
        }
    }

}
Jay
  • 32,092
  • 17
  • 52
  • 78
  • The OP doesn’t want to use a async await and isn’t using Firestore. They want a DispatchQueue/Group flow with realtime database written for them. – lorem ipsum May 27 '22 at 21:23
  • @loremipsum Hey! Good catch! I got so caught up in deciphering the question I just went with Firestore. I now added the code for the Realtime Database too! At this point there's no reason to not use the async calls as they work very well and in many cases, offers better code readability and flow than DispatchGroups. And the OP specifiically stated *So, how can I make the code "wait to complete"?* But, for you, I didn't use them in the RTDB code (although they are available). – Jay May 27 '22 at 21:59