Learn Courses My Dashboard

Can't get value out of completion

Hi everyone,

So I’m currently having some issues with these blocks of code:

  • A function to get the requested user by using their id’s. This function firstly checks the local users who have already been fetched, and then, if not all the users could be found in the local users, it checks the database where all the users are stored.
  • A function to fetch the requested users from databases using their id’s. So this is the function that is used in the function above.

Here are my two function:

The first function which check the local users and calls the other function

/// Given a list of user ids, returns a list of user objects with the same user id, and fetches any users who are not a contact
    func getUsers(ids: [String]) -> [User] {
        
        // Ids representing contacts
        var contactIds:[String] = []
        
        // Ids representing other users
        var userIds:[String] = []
        
        // The merged array of users
        @State var foundUsers: [User] = []
        
        // Put these ids in the right array
        for id in ids {
            
            switch self.userIsContact(id: id) {
            case true:
                contactIds.append(id)
                
            case false:
                userIds.append(id)
            }
        }
        
        if !contactIds.isEmpty {
            
            // Get these users
            foundUsers.append(contentsOf: self.getContacts(ids: contactIds))
        }
        
        if !userIds.isEmpty {
            
            // Fetch these users
            // Here is the call to my second function
            databaseService.getUsers(ids: userIds) { users in
                
                // Here is the issue

                // Append these users to the found users
                foundUsers.append(contentsOf: users)
            }
        }
        
        // Set these users to the users const. in the main thread
        return foundUsers
    }

The second function which performs a query to the database and returns the found users

/// Retrieves all the users by the given ids
    func getUsers(ids: [String], completion: @escaping ([User]) -> Void) {
        
        if !(ids.isEmpty) {
            
            // Create query
            let query = Firestore.firestore().collection("users").whereField(FieldPath.documentID(), in: ids)
            
            // Perform the query
            query.getDocuments { snapshot, error in
                
                if snapshot != nil && error == nil {
                    
                    // Convert the document into users
                    let users: [User] = snapshot!.documents.map { (try? $0.data(as: User.self)) ?? User() }.filter { $0.id != nil }
                    
                    // Return the ussers
                    completion(users)
                }
            }
            
        } else {
            
            completion([User]())
            
        }
        
    }

Now my problem with this code is in the first function, in the completion of the seconds function. (I’ve indicated this place in the code.)
The problem is the following: when I assign the value I get in this completion, being users, to the foundUsers array, the value is actually assigned to that variable. But when the code after the completion is ran, the value which was assigned to foundUsers in the completion, is disappeared. So the variable doesn’t hold that data anymore.

I don’t know how this happens, but it happens most of the time when I use a completion to return fetched data.

So if anyone could help me figure this out, it would be greatly appreciated!

In your
func getUsers(ids: [String]) -> [User] {

you have the declaration

@State var foundUsers: [User] = []

remove @State since State is a property wrapper that is only applicable in a SwiftUI View.

Hi @Chris_Parker,

I have removed the @State, but still have the same issue…

@Pixel

Yeah I wasn’t sure if that would solve the issue for you.

It’s a bit had to figure out what is going on in isolation from the entire project. Have you set breakpoints in your code to examine what data you expect to have at various points?

@Chris_Parker

Yes, I have. Actually, everything works fine, and even on this line of code: foundUsers.append(contentsOf: users) in the completion, the user values get assigned to foundUsers. But when I’m on the line to return them: return foundUsers, the values are gone and foundUsers is empty.

Also, I think for you to recreate the issue, you need to have a firebase database connected and perform queries to it with my functions.

Hello @Pixel

It looks like you’re experiencing an issue with asynchronous code. The problem you’re encountering is that the databaseService.getUsers function is making an asynchronous call to the database, which means that the code inside the completion block (where you’re appending the users to the foundUsers array) may not be executed until after the return foundUsers statement at the end of the getUsers function has already been executed.

One way to fix this would be to make the getUsers function itself asynchronous and use the await keyword to wait for the completion block to finish before returning the foundUsers array.

Here’s an example of how that might look:

func getUsers(ids: [String]) -> [User] {
    // ...
    if !userIds.isEmpty {
        // Fetch these users
        let fetchedUsers = await databaseService.getUsers(ids: userIds)
        foundUsers.append(contentsOf: fetchedUsers)
    }
    // ...
    return foundUsers
}

and the second function should be like this

func getUsers(ids: [String]) -> Future<[User], Error> {
    // ...
    var users: [User] = []
    let promise = Promise<[User]>()
    if !(ids.isEmpty) {
        // Create query
        let query = Firestore.firestore().collection("users").whereField(FieldPath.documentID(), in: ids)
        // Perform the query
        query.getDocuments { snapshot, error in
            if snapshot != nil && error == nil {
                // Convert the document into users
                users = snapshot!.documents.map { (try? $0.data(as: User.self)) ?? User() }.filter { $0.id != nil }
                promise.resolve(users)
            }else{
                promise.reject(error!)
            }
        }
    }else{
        promise.resolve([User]())
    }
    return promise.futureResult
}

This way the function will wait for the completion block to finish before returning the foundUsers array, which should ensure that the foundUsers array contains the data from the users array.

Alternatively, you can use a callback pattern or a promise pattern to handle the async code.

Hope this help. Good luck with your project. :blush:

1 Like

Yes that would have been an ideal situation but would have required a lot of set up work.

Hi @joash,

Many thanks for your helpful reply, I now fully understand what is happening and why my code doesn’t work as expected.

As upon trying what you proposed, I get the error that Promise is unknown. Do I need to add a package dependency for this or do I need to import some kind of framework?

The only thing I’ve found is FBLPromises framework, but that is not what you’re using, and upon trying I noticed that my User object should be a class, which is not ideal.

Hi @Chris_Parker ,

I fully understand, just wanted to give you a better understanding.

Hi @joash and @Chris_Parker

So I have been working and this is what I’ve achieved:

I have found a way to implement the code proposed by @joash, tested it and I found that it worked.
So actually the function should now work as expected, so thank you very much again for helping me.

However, implementing the function has now become difficult. I want to use the second function, which is now fixed, in the first function, but because the second function is asynchronous, I need to put it in a Task closure, I think. But when I do that I can’t make a connection to foundUsers. These are the errors I get from this:

Reference to captured var 'userIds' in concurrently-executing code
and
Mutation of captured var 'foundUsers' in concurrently-executing code

I don’t know how to fix this issue or if there is another way to implement the second function which is asynchronous. And I know I could also make the first function asynchronous, but in my experience that just replicates the problem in other places where I call this first function.

I hope this isn’t to confusing and you understand what I’m trying to say, and thank you so much for helping me out!

Hi @Pixel

May we ask if you can share your project with us? It’s hard to figure out without looking to your code. Maybe if you can share it with us, we can have a look on it, play with it and try some solutions. If you have a github repository you can add me as a contributor using my username emptybasket.

Hi @joash,

Thanks for your fast reply!
I understand that it would be much more helpful if I’d share the project to you.

I created a Github repository for the project and added you as a collaborator, here is the invite link: https://github.com/RunePollet/swiftui-chat/invitations.

If you’re able to pull the code and get my project, let me lead you to the problem in the project.
You can find the code we were talking about by going to swift-chat > swift-chat, to find the first function go to ViewModels > ContactsViewModel and scroll down to line 270 where you should get these two errors:
Reference to captured var 'userIds' in concurrently-executing code
Mutation of captured var 'foundUsers' in concurrently-executing code
To find the second function, you must go to Services > DatabaseService and scroll down to line 306 where the second function starts.

I think you will have to add a firebase database, although I don’t know how that works on Github. If you need more information about the Firebase SDK’s I’m using or other information, feel free to ask me.

I hope you are now able to recreate my problem and investigate for yourself.
Thank you for your help and if there’s anything else you need, please say so.

@joash, did it work for you?

Hi @Pixel I tried to check the code you shared to me, but haven’t successfully make it work on my side. My apology. Are you able to find a solution for your problem?

Hi @joash, unfortunately, I haven’t found a way to solve my problem so far…
Are you unable to recreate the problem I described or can’t you get the project to work properly aside from the errors we’re talking about?