getting a fully populated array outside the trailing closure after asynchronous function finishes executing in Swift - swift

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
}
}

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)

Related

Why is My Code running this way and is There a better way to solve this issue

// In the code below I am trying to return an array from data in firestore, the array always returned empty when I put the handler outside the for loop so I had to use an if statement inside the for loop to get the array containing the data. after using the print statement you see in the code i found out that the compiler is going over the entire function before entering the for loop, (print("5") & (print("6") are the first to run and when I put the handler outside the for it will also be triggered and return an empty array
**
func getMyGames(joinedGamesIDs: [String], handler: #escaping(_ games: [GameViewModal]) -> ()) {
var games = [GameViewModal]()
if !joinedGamesIDs.isEmpty{
for id in joinedGamesIDs {
db.collection("games").document(id).getDocument { (document, error) in
if let document = document, document.exists {
if let game = self.getGameViewModal(document: document){
games.append(game)
print("1")
print(games.count)
}
print("2")
print(games.count)
}
print("3")
print(games.count)
if games.count == (joinedGamesIDs.count){
handler(games)
}
print("4")
print(games.count)
}
}
print("5")
print(games.count)
}
print("6")
print(games.count)
}
**
I've embedded my explanations in the code commentary for easier reading. But the problem you have is that you aren't coordinating these async tasks (the getting of each document). You must coordinate them so when the last one finishes, you can "return" the array from the function. This function doesn't technically "return" anything (except Void) but the completion handler, in a way, "returns" the array which is why I put it in quotes. These semantic details matter and it helps to understand everything better.
func getMyGames(joinedGamesIDs: [String], handler: #escaping (_ games: [GameViewModel]) -> ()) {
guard !joinedGamesIDs.isEmpty else {
// If there is nothing to do, always consider
// calling the handler anyway, with an empty
// array, so the caller isn't left hanging.
return handler([])
}
// Set up a Dispatch Group to coordinate the multiple
// async tasks. Instatiate outside of the loop.
let group = DispatchGroup()
var games: [GameViewModel] = []
for id in joinedGamesIDs {
// Enter the group on each iteration of async work
// to be performed.
group.enter()
db.collection("games").document(id).getDocument { (document, error) in
if let doc = document,
doc.exists,
let game = self.getGameViewModal(document: doc) {
games.append(game)
} else if let error = error {
// Always print errors when in development.
print(error)
}
// No matter what happens inside the iteration,
// whether there was a success in getting the
// document or a failure, always leave the group.
group.leave()
}
}
// Once all of the calls to enter the group are equalled
// by the calls to leave the group, this block is called,
// which is the group's own completion handler. Here is
// where you ultimately call the function's handler and
// return the array.
group.notify(queue: .main) {
handler(games)
}
}

Can't assign a value to variable inside of closure Swift [duplicate]

This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 2 years ago.
I'm trying to get current document count belongs to spesific user from Firebase.
Whenever i try to assign count value to my variable in closure, value is always shown as nill.
So that i did couple of research, as a result i figured out networking sometimes takes so long and it happens asynchronously. So if i'm not wrong due to asynchronous returning a value inside a fucntion might happen before assign value .
I tried to add dispatchqueue.main.async but it didn't work for me...
Here is my code
func getEventCount () -> Int? {
var count: Int?
db.collection("Events").whereField("owner", isEqualTo: currentUser.email).getDocuments { (snapshot, error) in
if error != nil {
print(error)
}else {
DispatchQueue.main.async {
if let snapshot = snapshot {
count = snapshot.count
}
}
}
}
return count
}
My main goal is getting count data from database and assign it to 'count' variable.
So why do i need that count variable? Because i'm going to pass that count value to tableView datasource method numberOfRowsInSection which is expecting a int value. With that value i'm going to represent some data in Events document from firestore in table views.
note: When i try to print count value in closure it shows desired value, but when function return value it's shown nill...
Once it is a Async call - you cannot synchronously return the value from the function. You should accept a callback to the function that will accept the count. That callback function or closure will be passed the value asynchronously.
func getEventCount (callback: #escaping(Result<Int, Error>) -> Void) {
db.collection("Events").whereField("owner", isEqualTo: currentUser.email).getDocuments { (snapshot, error) in
if error != nil {
let result = Result.failure(error)
callback(result)
}else if let snapshot = snapshot {
let result = Result.success(snapshot.count)
callback(result)
} else {
let result = Result.failure(SomeCustomAppError)
callback(result)
}
}
}
Then you can call this function passing in a callback
self.getCount() { result in
switch result {
case .success(let count): /// use count
print(count)
// only here u can assign the count value to ur variable
case .error(let error): /// handle error
print(error.localizedDescription)
}
}
Note: In the above I've used the Result datatype from Swift standard library - https://developer.apple.com/documentation/swift/result so that both error
or result can be passed back

object content different from within method

I am encountering a strange behavior with an App I am building.
I have a struct Record and I create an instance of it from a NavigationViewController's child ViewController.
We can see it like this:
NavigationController (called TaskTabsController)
|
ViewController (called TaskFromController)
If I put a breakpoint, I can inspect and check the content of the object is what it should (conform to interface input data).
But when I call a method on that instance (the very next line), the different members have different or missing values.
The object instance is created from within TaskTabsController with something like so:
if let formVC = (viewControllers?[1] as? TaskFormViewController) {
let rec: TaskRecord? = formVC.makeTaskRecord()
// here the rec data are correct when I inspect the instance
rec?.prepareData()
// from the prepareData function, the properties are different of can't be accessed...
}
Ex: From controller, I can see my instance rec having a member task instance (of Task type) with a name property.
But from within the prepareData method, that task member can't display the name attached to it. (debugger says value unreadable or something like that)
Also, I can see a list of other objects, but in the method, their count is different...
Here is how makeTaskRecord method works: (from my TaskFormViewController)
In TaskFormFiewController I have a private property like so:
private var stepsSelected: [StepRecord] = []
That property is updated whit user actions.
The StepRecord is a struct. (so should be passed by value I think)
Next is the makeTaskRecord method in the same controller (gathering details from form elements)
func makeTaskRecord() -> TaskRecord? {
guard let current_task = task
else {
print("TaskForm.makeTaskRecord(): Can't create a record without a Task")
return nil
}
// check data validity
var errors:[String] = []
// generate StepRecords >>>
if stepsSelected.count == 0 {
errors.append("Error, no step selected")
}
// <<<
// DATA OK
if errors.isEmpty {
let taskrecord = TaskRecord(
id: record?.id,
taskId: task!.id,
userCode: Config.getUser()!.code, // global object containing login state
pictures: [],
memo: memo.text,
task: current_task,
stepRecords: stepsSelected // data here is correctly set
)
return taskrecord // but here, I can't retreive the stepRecords! (1)
}
else {
// display errors in an alert box
let errorVC = MyHelpers.makeAlert("Error with data", errors.joined(separator: "\n"))
self.present(errorVC, animated: true)
return nil
}
}
Here is how the prepareData() method looks like: (method of Record class)
func prepareData() -> [String: Any]? {
// no steps in record, data is invalid
guard
let stepRecordsJson = try? JSONEncoder().encode(self.stepRecords)
// This is where I constated that stepRecords is different (set but empty)
else {
return nil
}
// other data
var params = [
"task_id": taskId,
"user_code": userCode,
"step_records": stepRecordsJson, // this is what doesn't work well, but it's certainly related to (1) above
"memo": memo ?? ""
] as [String : Any]
// when editing, we set again the record ID
if let edit_id = self.id {
params["id"] = edit_id
}
return params
}
Note: All the data structures and Codable struct
In the sample above, at the (1), I put a breakpoint on the return line.
When checking data, I see that the "sub-struct" do not have good values:
(lldb) print current_task.name
(String) $R0 = "屋根工事"
(lldb) print taskrecord.task.name
error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x6f).
The process has been returned to the state before expression evaluation.
The task is a struct so there should not be problems about it, but the debugger can't read the task I assigned to my taskrecord.
Is there something I am missing? Like a pointer reference of some sort?

Function returns null value

This might be a basic question but I am unable to figure this out. My function always returns a null value and I am not sure why.
My main code ( in a different view controller) calls this function with a "string" to search. But at the end of the function, I can never return the results. If i print the search results where I have the final print statement, then I do see the results. But right after that function is complete, the return value is nil. How would I get the return value from this function ? Thanks in advance,. I am using Swift btw.
func fetchSearchResults (_ searchString: String) -> Array<Any> {
self.globalPost = [Posts]()
self.postKeysArray = []
var searchIndex:AlgoliaSearch.Index
let query = Query()
searchIndex = self.client.index(withName: "Comments")
// you can ignore all of this below until the last couple lines...
query.attributesToRetrieve = ["userComment","postId"]
query.attributesToHighlight = ["postId"]
query.query = searchString
query.optionalWords = [searchString]
let curSearchId = searchId
searchIndex.search(query, completionHandler: { (content, error) in
if (curSearchId <= self.displayedSearchId) || (error != nil) {
return // Newest query already displayed or error
}
self.displayedSearchId = curSearchId
self.loadedPage = 0 // Reset loaded page
guard let hits = content!["hits"] as? [[String: AnyObject]] else { return }
var tmp = [String]()
for hit in hits {
// print ("Comment is \(commentRecord(json:hit).postId)")
tmp.append(commentRecord(json:hit).postId!)
}
self.postKeysArray = tmp
self.getDatafromFirebase(searchString)
// so my search code returns some values here and i see them in the post keysArray.
print ("Search Results -post keys is \(self.postKeysArray)") \\ returns correct values here in the Array
})
self.searchId += 1
// But here i get a NULL value.
return self.postKeysArray \\ returns empty array
}
As Alexander noted, you are printing the search results inside the completion handler, which has not executed yet when fetchSearchResults returns. There are a couple different ways that you could solve this. One would be to refactor fetchSearchResults to take a closure:
func fetchSearchResults (_ searchString: String, completionHandler completion: (Array<Any>) -> ()) {
// ...
searchIndex.search(query, completionHandler: { (content, error) in
// ...
self.postKeysArray = tmp
self.getDatafromFirebase(searchString)
// so my search code returns some values here and i see them in the post keysArray.
print ("Search Results -post keys is \(self.postKeysArray)") \\ returns correct values here in the Array
completion(self.postKeysArray)
})
}
Then, in your 'main code', wherever you were calling:
let results = searchController.fetchSearchResults(searchString)
doSomething(with: results)
You would now have:
searchController.fetchSearchResults(searchString, { results in
doSomething(results)
})
This line could also be written:
searchController.fetchSearchResults(searchString, doSomething)
If you really can't have fetchSearchResults be asynchronous, then you could use DispatchSemaphore to wait until the completion handler has finished:
func fetchSearchResults (_ searchString: String) -> Array<Any> {
// ...
var semaphore = DispatchSemaphore(0)
searchIndex.search(query, completionHandler: { (content, error) in
// ...
self.postKeysArray = tmp
self.getDatafromFirebase(searchString)
// so my search code returns some values here and i see them in the post keysArray.
print ("Search Results -post keys is \(self.postKeysArray)") \\ returns correct values here in the Array
semaphore.signal()
})
semaphore.wait()
return self.postKeysArray
}
Note that unless you know what you're doing, using semaphores is not a good idea. If you don't know what threads various callbacks might be called on, it can lead to a deadlock. The above example will not work if the searchIndex.search completion handler is called on the same DispatchQueue as fetchSearchResults. If you don't understand what I'm talking about, you probably shouldn't do it. It will almost certainly be better to use asynchronous callbacks.

How to return values from Haneke's async fetch method

I'm trying to parse some data that I cached using Haneke Swift. I've cached the data and have written the parser to accomplish this. This parser is in a separate class called AssembleCalendar().
Using Haneke's example code for fetching, I've tried with complete and utter failure to actually return a value from the closure.
My attempt
func getScheduledItems() -> [ScheduledItem] {
var scheduledItem = [ScheduledItem]() // initialize array
let cache = Shared.dataCache
cache.fetch(key: "CalendarData").onSuccess { data in
scheduledItem = AssembleCalendar().assimilate(data) // update array
print(scheduledItem) // array returns expected value
}
print(scheduledItem) // array returns nil
return scheduledItem // returns nil
}
What I know
I understand that this is an asynchronous issue. My code isn't waiting for my AssembleCalendar() parser to finish. It's just running each line and returns nil long before my scheduledItem receives a value. I've tried many, many solutions and read quite a few examples online but I cannot figure out how to retrieve a value from this closure in this scenario.
My question
How can I get .fetch() to return a value before my function hits nil?
update:
Here's my code in context:
class Schedule {
var items : [ScheduledItem]
init() {
items = getScheduledItems() // Schedule.getScheduledItems()
}
func getScheduledItems(completion: (items: [ScheduledItem]) -> ()) {
var scheduledItem = [ScheduledItem]() // initialize array
let cache = Shared.dataCache
cache.fetch(key: "CalendarData").onSuccess { data in
scheduledItem = AssembleCalendar().assimilate(data) // update array
print(scheduledItem) // array returns expected value
completion(items: scheduledItem)
}
}
}
Fetch() is using a completion handler, this means that the block of code called in there is actually executing AFTER your return statement has been executed. This is why it is returning nil. To get around this, you can use your own completion handler to return the information:
class Schedule {
var items = [ScheduledItem]()
init(items: [ScheduledItem]) {
self.items = items
}
class func getScheduledItems(completion: (items: [ScheduledItem]) -> ()) {
var scheduledItem = [ScheduledItem]() // initialize array
let cache = Shared.dataCache
cache.fetch(key: "CalendarData").onSuccess { data in
scheduledItem = AssembleCalendar().assimilate(data) // update array
print(scheduledItem) // array returns expected value
completion(items: scheduledItem)
}
}
}
Then initialise the class using this:
Schedule.getScheduledItems { (items) -> () in
var schedule = Schedule(items: items)
}