0

I am trying to populate finalArray with the result of asynchronous call from my extractProperties() function.

class ViewController: UIViewController {

var finalArray: [SingleRepository] = []

let extractor = Extractor()

override func viewDidLoad() {
    super.viewDidLoad()

    print("Starting the program... ")

    extractor.extractProperties { object, error in
        guard let object = object else {
            print("Extractor did not reutrn data")
            return
        }
        self.finalArray.append(object)
        print("Appended successfully --- \(self.finalArray.count) --- inside the trailing closure")

    }

    print("Size of the array --- \(self.finalArray) --- outside the trailing closure")


}

The issue is that I can't get the fully populated finalArray to work with outside the scope of trailing closure! Output log:

    Starting the program... 
Size of the array --- [] --- outside the trailing closure
Appended successfully --- 1 --- inside the trailing closure
Appended successfully --- 2 --- inside the trailing closure
Appended successfully --- 3 --- inside the trailing closure
.
.
.
Appended successfully --- 300 --- inside the trailing closure

I know why the print statement from outside get executed first, but I never can get my fully populated array with all 300 objects in it.

Please note that the following post did NOT solve my problem:Run code only after asynchronous function finishes executing

I even tried solution in that post by writing a the following function:

func constructingFinalArray(completionBlock: @escaping ([SingleRepository]) -> Void) {
        var fArrray: [SingleRepository] = []
        extractor.extractProperties { data, error in
            guard let data = data else {
                print("Extractor did not reutrn data")
                return
            }
            fArrray.append(data)
            completionBlock(fArrray)
        }
    }

and called it inside viewDidLoad() as following, but confusingly I got the same result and array gets populated element by element, therefore never be able to access a fully populated array from out of trailing closures!

constructingFinalArray { array in
        print("array size from constructingFinalArray function: \(array.count) ")
    }

output:

    Starting the program... 
array size from constructingFinalArray function: 1 
array size from constructingFinalArray function: 2 
array size from constructingFinalArray function: 3
.
.
.

extractProperties get called 300 times exactly and sometimes it returns no date(Error).

    // Class to extract the required properties and
// provide ready to use object for ViewController
class Extractor {
    private let client = RepoViewerAPIClient()
    private var allURLs: [RepositoryURL] = []
    var allRepositories: [SingleRepository] = []


    // Method to extract all the required properties
    // compromising of 2 asynchrounous call, (nested asynch call)
    // one to get the urls and another call within the first call to extract all the propreties
    func extractProperties(completionHandler: @escaping (SingleRepository?, RepoViewerErrors?) -> Void) {
        // implementation of nested asynchronous calls are deleted to shorten the question length 


    }

}
Ali_C
  • 215
  • 3
  • 11
  • Somehow related question: so you are calling `extractor.extractProperties` only once and the closure is called 300 times? – Luca Angeletti Aug 19 '18 at 19:24
  • yes, I did not post the implementation of extractProperties because that is a long and complex function compromising two nested asynchronous calls in my other class and you can assume it returns 300 objects of type SingleRepository. I need the fully populated array to have all of 300 objects. – Ali_C Aug 19 '18 at 19:39
  • @Ali_C How does the extractor notify that it is finished? Without this and not knowing how many are to come the problem is ugly since its a guessing game whether all turned up already or not. – Fabian Aug 19 '18 at 19:41
  • You could add a `finished: @escaping () -> ()` callback as an argument and call that after the completion-callback got called the last time, maybe after the for-loop. Then the number of objects does not matter. – Fabian Aug 19 '18 at 19:49
  • @Purpose could you elaborate more ? are you suggesting to change the function signature in Extractor class? – Ali_C Aug 19 '18 at 20:20
  • @Ali_C If the number of objects may change, yes. One callback for each objects and one callback to tell outside that its finished. Like [CKFetchRecordZoneChangesOperation](https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation), where you can see under „Processing Operation Results“ how one can do it. Then you can implement a wrapper function sanely where you fill an array till finished is called, in which you call the wrappers‘ completion block with all the objects collected in the array. – Fabian Aug 19 '18 at 20:34

1 Answers1

4

It seems that after you call once

extractor.extractProperties {
    ...
}

The closure gets called exactly 300 times but sometimes it can return no data.

In this case you can follow this approach.

extractor.extractProperties { object, error in
    serialQueue.async { [weak self] in

        count += 1

        guard count < 300 else  {
            self?.didCompletePopulation()
            return
        }

        guard let object = object else {
            print("Extractor did not reutrn data")
            return
        }

        self?.finalArray.append(object)

    }
}

func didCompletePopulation() {
    // be aware, this is not called on the main thread
    // if you need to update the UI from here then use the main thread
    print("Final array is populated \(self.finalArray)")
}

How does it work?

The body of the closure is wrapped is wrapped into another closure executed through a Serial Queue. This way we are sure the share resources (finalArray and count) are accessed safely.

serialQueue.async { [weak self] in
    ...
}

Next every execution of the closure increments count by 1.

Then we make sure count is minor than 300, otherwise we stop the execution of the closure an call didCompletePopulation().

guard count < 300 else  {
    self?.didCompletePopulation()
    return
}

We check if the result contains a proper value otherwise we stop the execution of the current closure

guard let object = object else {
    print("Extractor did not reutrn data")
    return
}

And finally we add the new element to the array

self?.finalArray.append(object)
Luca Angeletti
  • 58,465
  • 13
  • 121
  • 148
  • I believe hypothesis 2 is the case for my problem and you are right, I should have posted my extractor class s well, but my question got so long and I tried to be as concise as possible. I will update my question to provide the Extractor class as well. – Ali_C Aug 19 '18 at 19:44
  • @LucaAngeletti Is the serial queue for synchronization of access to the array? – Fabian Aug 19 '18 at 19:44
  • @Purpose Yes, since Swift Array is not thread safe and it is being written by multiple concurrent threads, I needed a way to perform one insertion at the time. – Luca Angeletti Aug 19 '18 at 19:46
  • @LucaAngeletti I appreciate the answer, I tried the 2b case and I assumed the count variable is something I need to define before extractProperties which start from 0. In that case the solution does not work for me. Considering the fact I just print on console and don't work with UI yet, didCompletePopulation never gets called. – Ali_C Aug 19 '18 at 20:25
  • @Ali_C It means that “extractor” is not calling the closure 300 times – Luca Angeletti Aug 19 '18 at 20:33
  • @LucaAngeletti well I put a print statement right after the line "count +=1" to print the count variable, and the output start from 1 and ends at 300. count after increament: 1 ... count after increament: 300 which means extractor get called 300 times exactly. Am I missing something ? – Ali_C Aug 19 '18 at 20:50
  • @Ali_C Sorry I think I made a mistake. I’m editing my answer now from my phone – Luca Angeletti Aug 19 '18 at 21:10
  • @Ali_C Now should be fixed. Let me know if it works – Luca Angeletti Aug 19 '18 at 21:13
  • @LucaAngeletti It works only if I set the condition to count == 300 and now didCompletePopulation gets called on every run of the application. I always get the count of final array as 299 instead of 300, but that might be an issue in my own code. I highly appreciate the effort. I have also tried hypothesis one for the sake of completeness which worked as well. – Ali_C Aug 19 '18 at 22:52
  • @Ali_C Good, I will rewrite my answer leaving only the scenario 2B in order to make the whole explanation more clear. – Luca Angeletti Aug 20 '18 at 07:51
  • @LucaAngeletti while this is a perfect working solution, however I still need to perform some action on the final array such as filter function. The problem is that I can only do it inside the didCompletePopulation which means every time i need to filter, all the calls have to be made again. Is there any way of accessing the fully populated final array out of the scope of didCompletePopulation, within the viewDidLoad()? – Ali_C Aug 20 '18 at 13:47
  • @Ali_C No. Because `Interactor.extractProperties` has an asynchronous callback. So (potentially) `finalArray` is ready after `viewDidLoad` has been completed. If you need `finalArray` to be ready to be used in the `viewDidLoad` then load it **before** loading the current ViewController. – Luca Angeletti Aug 20 '18 at 13:52