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?

Can you start from the beginning? What’s the issue? To get a value out of a completion handler, you use the completion handler at the call site.

You can add me to the project if you want, and if you can explain which files you’re looking at specifically and what value you need where, I can try to look

GitHub: mikaelacaron

Hi @mikaelacaron,

Thanks for offering your help!
I will try to explain what’s happened as good as I can.

So, the issue I created this topic with was the following:
Due to my function getUsers, I was making an asynchronous call, so when the function would return a value through its completion block, I would then assign it to a variable in another function (I will call this function function1 because the 2 functions in matter, have the same name) and return that variable from function1. Of course due to the asynchronous function, getUsers or function2, the completion block will be run asynchronous to function1, so function1 can’t return the value from function2.

Now with the answer of @joash, I understood this problem, but his solution didn’t fully work for me due to some errors, anyway I’ve done some research and implemented the idea from @joash, being to make function2 asynchronous through a future and a promise so I could actually return the value from function2 and await it in function1. After implementing this, my functions look like the following:

function2 (the asynchronous one):

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

        let future = Future<[User], Never>() { promise in

            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 documents into users
                        let users: [User] = snapshot!.documents.map { (try? $0.data(as: User.self)) ?? User() }.filter { $0.id != nil }

                        // Return them
                        promise(.success(users))
                    } else {
                        print("ERROR SD-326: \(error!.localizedDescription)")
                    }
                }
            } else {
                promise(.success([User]()))
            }

        }

        return await future.value
    }

function1:

func getUsers(ids: [String]) -> [User] {
        
        // Ids representing contacts
        var contactIds:[String] = []
        
        // Ids representing other users
        var userIds:[String] = []
        
        // The merged array of users
        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
            // MARK: On this line I am calling function2
            let users = await databaseService.getUsers(ids: userIds)
                
            // Append these users to the found users
            foundUsers.append(contentsOf: users)
            
        }
        
        // Set these users to the users const. in the main thread
        return foundUsers
        
    }

Now the problem is somewhat fixed, but another one has occurred. Let me explain it.
In function1 from above, I got the following error:
'async' call in a function that does not support concurrency
on this line:
let users = await databaseService.getUsers(ids: userIds).

The option provided by the error message was to ad the async keyword in the function declaration, but if I’d do that, I’d have to put await before every call of function1, and then I’d get the same error about concurrency and I’d end up in some kind of loop. I don’t know how to prevent that but if you’d have an idea, that would be great!

But what I did is wrapping the following lines in a Task container:

Task {
   // Fetch these users
   // MARK: On this line I am calling function2
   let users = await databaseService.getUsers(ids: userIds)
                
   // Append these users to the found users
   foundUsers.append(contentsOf: users)
}

So function1 does now look like this:

func getUsers(ids: [String]) -> [User] {
        
        // Ids representing contacts
        var contactIds:[String] = []
        
        // Ids representing other users
        var userIds:[String] = []
        
        // The merged array of users
        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 {
            
            Task {
                // Fetch these users
                // MARK: On this line I am calling function2
                let users = await databaseService.getUsers(ids: userIds)
                
                // Append these users to the found users
                foundUsers.append(contentsOf: users)
            }
            
        }
        
        // Set these users to the users const. in the main thread
        return foundUsers
        
    }

But, I’m now stuck with the following two errors, and I don’t know how to fix them or how I could address this problem in another way:
Reference to captured var 'userIds' in concurrently-executing code
and
Mutation of captured var 'foundUsers' in concurrently-executing code.

I hope you now understand what the issue is and what happened, although I know it is a little bit complicated.

If you’d like to get my project, I’ve added you as a collaborator on Github: 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.

I’ll take a look later today!

1 Like

I’ve looked over a little. I can’t immediately build it, but I haven’t looked in-depth why.

I’ll add a PR with a small change, but basically this stems from, I think you’re calling some code in places you shouldn’t be.

getUsers in DatabaseService is async and is called in getUsers in contactsVM and used in several places, where it is not asynchronous.

But how it’s put together. It may have been better to keep it async, and move some logic around, instead of handing async / non-async code.

You don’t need futures and promises, but you can if you want. Using closures and result types has been what I’ve usually done, cause futures and promises confuse me :joy:

Personally, I don’t go this in-depth with helping on the forum, but I do personal mentoring, to look more in-depth and possibly move logic around. If you’re interested, you can DM me

Hi @mikaelacaron,

Thank you very much for your reply and explanation.

I understand that my code isn’t that logic and I will try to improve that.
And thank you for the PR that you would send so I can see for myself how it should be done better, however I haven’t seen or found it on Github yet…