Search code examples
swiftswiftuicloudkitcompletionhandler

Nested Cloudkit queries not printing in the right order


My code is not printing in the right order. I think its a problem with the way I'm using dispatch to main. I want to pass in a boss ID. Find the User with that ID then get their SubscribedBosses Array. For every ID in that array query that user and return that users screenName. Append those screenName to the userArray. After that Complete the GetBossSubs function and return userArray (array of screenNames).

Right now result of the .onAppear is running before the function in .onAppear is actually completed. User Appended prints before Found TestUserName

static func getBossSubs(bossID: String, completion: @escaping (Result<UserNames, Error>) ->
    ()) {
    let pred = NSPredicate(format: "uniqueID = %@", bossID)
    let sort = NSSortDescriptor(key: "creationDate", ascending: false)
    let query = CKQuery(recordType: RecordType.Users, predicate: pred)
    query.sortDescriptors = [sort]
    
    let operation = CKQueryOperation(query: query)
    operation.desiredKeys = ["subscribedBosses"]
    operation.resultsLimit = 50
    
    operation.recordFetchedBlock = { record in
        DispatchQueue.main.async {
            guard let subs = record["subscribedBosses"] as? [String] else {
                print("Error at screenName")
                
                completion(.failure(CloudKitHelperError.castFailure))
                return
            }
            let userArray = UserNames() //Error that it should be a LET is here. 
            for boss in subs{
                CloudKitHelper.getBossScreenName(bossID: boss) { (result) in
                    
                    switch result{
                        
                    case .success(let name):
                        userArray.names.append(name) //works fine
                        print("Found \(userArray.names)") //Prints a name
                    case .failure(let er):
                        completion(.failure(er))
                    }
                    
                }
                
                
            }
            print("does this run?") // Only runs if No Name
            completion(.success(userArray)) // contains no name or doesn't run?
        }
    }
    operation.queryCompletionBlock = { (_, err) in
        DispatchQueue.main.async {
            if let err = err {
                completion(.failure(err))
                return
            }
        }
        
    }
    CKContainer.default().publicCloudDatabase.add(operation)
}

I call the code like this:

.onAppear {
            // MARK: - fetch from CloudKit
            self.userList.names = []
            let myUserID = UserDefaults.standard.string(forKey: self.signInWithAppleManager.userIdentifierKey)!
            // get my subs projects
            CloudKitHelper.getBossSubs(bossID: myUserID) { (results) in
                switch results{
                    
                case .success(let user):
                    print("Users appended")
                    self.userList.names = user.names
                case .failure(let er):
                    print(er.localizedDescription)
                }
            }
        }

Solution

  • This is because you run an asynchronous function inside another asynchronous function.

    // this just *starts* a series of asynchronous functions, `result` is not yet available
    for boss in subs {
        CloudKitHelper.getBossScreenName(bossID: boss) { result in
            switch result {
            case .success(let name):
                userArray.names.append(name) // works fine
                print("Found \(userArray.names)") // Prints a name
            case .failure(let er):
                completion(.failure(er))
            }
        }
    }
    
    // this will run before any `CloudKitHelper.getBossScreenName` finishes
    completion(.success(userArray))
    

    A possible solution may be to check if all CloudKitHelper.getBossScreenName functions finish (eg. by checking the size of the userArray) and only then return the completion:

    for boss in subs {
        CloudKitHelper.getBossScreenName(bossID: boss) { result in
            switch result {
            case .success(let name):
                userArray.names.append(name)
                if userArray.names.count == subs.count {
                    completion(.success(userArray)) // complete only when all functions finish
                }
            case .failure(let er):
                completion(.failure(er))
            }
        }
    }
    
    // do not call completion here, wait for all functions to finish
    // completion(.success(userArray))