This question already has answers here:
Run code only after asynchronous function finishes executing
(2 answers)
Closed 7 years ago.
I am trying to figure out how to return a value from inside a nested function to its parent function. The code that I am working with is:
func findObjectsInBackgroundFromLocalDataStoreIfPossible (query: PFQuery, toCallUponCompletion: () -> ()) -> [PFObject]{
var response = [PFObject]()
let queryCopy = query.copy() as! PFQuery
queryCopy.fromLocalDatastore()
queryCopy.findObjectsInBackgroundWithBlock{
(objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil{
if objects?.count == 0{
query.findObjectsInBackgroundWithBlock{
(objects2: [AnyObject]?, error: NSError?) -> Void in
if error == nil{
response = objects2 as! [PFObject]
}
}
}
else{
response = objects as! [PFObject]
}
}
toCallUponCompletion()
}
}
I need to return the response from inside the function because it can return only once the query has finished, but if I try to do this, the compiler complains that I cannot do this because the return type of the nested function is Void. Is there Swift syntax that allows you to return a value directly to the "parent" function from inside a nested function?
Thanks. Any help is greatly appreciated.
You misunderstand how async calls with completion blocks/closures work.
You can't return a result in your function because at the time the function returns, the result doesn't exist yet.
The function findObjectsInBackgroundFromLocalDataStoreIfPossible takes a closure (a block of code) as a parameter. It returns immediately, before the background network request has even been sent, much less processed.
You then forget about the background job and wait. Once the background task has completed, it calls your closure.
Related
I'm trying to query a class of objects with specific ID's to delete. When I run it however, I get an error for "This query has an outstanding network connection. You have to wait until it's done". I assume this is because I'm trying to delete an object while I'm still accessing it.
I've provided the deletion code in the closure because I'm assuming the closure statements only execute after the function call finishes, yet it's still giving me the error. I've also tried using DispatchGroups because it does seem like a concurrency issue, but I'm not too familiar with their usage yet. Here's my code:
let idList = [...] // Some list of ID's I would like to remove
let query = PFQuery(className: "Pictures")
for id in idList {
query.getObjectInBackground(withId: id) { (object: PFObject?, error:
Error?) in
if error == nil {
img?.deleteInBackground() { (success, error: Error?) ...
}
}
I'm expecting each object associated with an ID in my original IdList to be deleted from the Parse backend. However, it seems that getObjectInbackground() and deleteInBackground() are clashing. If anyone could provide some advice, that would be wonderful!
You can try
var idList = [...]
func delete(_ id:Int) {
query.getObjectInBackground(withId: id) { (object: PFObject?, error: Error?) in
if error == nil {
img?.deleteInBackground() { (success, error: Error?) ...
}
idList = Array(idList.dropFirst())
if !idList.isEmpty {
delete(idList.first!)
}
}
}
Initially call it like
delete(idList.first!)
func startUpdates(from start: Date,
withHandler handler: #escaping CMPedometerHandler)
typealias CMPedometerHandler = (CMPedometerData?, Error?) -> Void
The above function retrieves the pedometer data from your iOS device. When I called the function the only argument I need passed to is the parameter from start.
Who actually initialized the parameter list of the completion handler closure? The startUpdates function I've called?
When I called the function the only argument I need to passed to is the parameter from start
That's not true.
You have to pass also the closure as second parameter. The closure itself is called by the startUpdates function after doing its work and passes two parameters back, an optional Data and an optional Error instance.
The functional programming is a very convenient way to be able to run arbitrary code (in the closure).
You can declare the closure separately
let result : CMPedometerHandler = { data, error in
if let error = error { print(error); return }
// do something with the data
}
startUpdates(from: Date(), withHandler: result)
or inline
startUpdates(from: Date(), withHandler: { data, error in
if let error = error { print(error); return }
// do something with the data
})
or with trailing closure syntax
startUpdates(from: Date()) { data, error in
if let error = error { print(error); return }
// do something with the data
}
Could anyone tell me why my startingPoints array is still at 0 elements? I know that I am getting objects returned during the query, because that print statement prints out each query result, however it seems like those objects are not getting appended to my local array. I've included the code snippet below...
func buildStartSpots() -> Void {
let queryStartingPoints = PFQuery(className: "CarpoolSpots")
queryStartingPoints.whereKey("spotCityIndex", equalTo: self.startingCity)
queryStartingPoints.findObjectsInBackgroundWithBlock{(objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
for object in objects! {
print("starting point: \(object)")
self.startingPoints.append(object)
}
} else {
// Log details of the failure
print("Error: \(error!) \(error!.userInfo)")
}
}
print("starting points")
dump(self.startingPoints)
}
While I have no experience in Parse, the block is asynchronously executed and likely non-blocking as dictated by the method name of the API call. Therefore, it is not guaranteed that the data would be available at the time you call dump, since the background thread might still be doing its work.
The only place that the data is guaranteed to be available at is the completion block you supplied to the API call. So you might need some ways to notify changes to others, e.g. post an NSNotification or use event stream constructs from third party libraries (e.g. ReactiveCocoa, RxSwift).
When you try to access the array, you need to use it within the closure:
func buildStartSpots() -> Void {
let queryStartingPoints = PFQuery(className: "CarpoolSpots")
queryStartingPoints.whereKey("spotCityIndex", equalTo: self.startingCity)
queryStartingPoints.findObjectsInBackgroundWithBlock{(objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
for object in objects! {
print("starting point: \(object)")
**self.startingPoints.append(object)**
}
//use it here
startingPoints xxx
} else {
// Log details of the failure
print("Error: \(error!) \(error!.userInfo)")
}
}
print("starting points")
dump(self.startingPoints)
}
I am able to get the application functioning as intended and will close this answer out.
It seems as though that the startingPoints array is not empty, and the values I need can be accessed from a different function within that same class.
The code snippet I am using to access my locally stored query results array is here:
for object in self.startingPoints {
let startingLat = object["spotLatitude"] as! Double
let startingLong = object["spotLongitude"] as! Double
let carpoolSpotAnnotation = CarpoolSpot(name: object.valueForKey("spotTitle") as! String, subTitle: object.valueForKey("spotSubtitle") as! String, coordinate: CLLocationCoordinate2D(latitude: startingLat, longitude: startingLong))
self.mapView.addAnnotation(carpoolSpotAnnotation)
The code snippet above is located within my didUpdateLocations implementation of the locationManager function, and with this code, I am able to access the query results I need.
I'm trying to generate an array of PFObjects called 'areaList'. I've been researching this quite a bit and understand that I could benefit from using a completion handler to handle the asynchronous nature of the loaded results. My ask, specifically, is to get some guidance on what I'm doing wrong as well as potential tips on how to achieve the result "better".
Here is my query function with completion handler:
func loadAreasNew(completion: (result: Bool) -> ()) -> [Area] {
var areaList = self.areaList
let areaQuery = PFQuery(className: "Area")
areaQuery.findObjectsInBackgroundWithBlock {
(areas: [PFObject]?, error: NSError?) -> Void in
if error == nil {
for area in areas! {
let areaToAdd = area as! Area
areaList.append(areaToAdd)
// print(areaList) // this prints the list each time
// print(areaToAdd) // this prints the converted Area in the iteration
// print(area) // this prints the PFObject in the iteration
if areaList.count == areas!.count {
completion(result: true)
} else {
completion(result: false)
}
}
} else {
print("There was an error")
}
}
return areaList
}
Here is how I'm attempting to call it in viewDidLoad:
loadAreasNew { (result) -> () in
if (result == true) {
print(self.areaList)
} else {
print("Didn't Work")
}
}
I assigned this variable before viewDidLoad:
var areaList = [Area]()
In the console, I get the following:
Didn't Work
Didn't Work
Didn't Work
Didn't Work
[]
Representing the 5 items that I know are there in Parse...
This is an interesting question. First off, PFQuery basically has a built in completion handler, which is quiet nice! As you probably know, all of the code within the areaQuery.findObjectsInBackgroundWithBlock {...} triggers AFTER the server response. A completion most often serves the purpose of creating a block, with the ability of asynchronously returning data and errors.
Best practice would (IMO) to just call the code that you want to use with the results from your PFQuery right after your area appending loop (which I'm gonna take out because I'm picky like that), like so:
func loadAreasNew() {
var areaList = self.areaList
let areaQuery = PFQuery(className: "Area")
areaQuery.findObjectsInBackgroundWithBlock {
(areas: [PFObject]?, error: NSError?) -> Void in
if error == nil {
let areasFormatted = areas! As [Areas]
areasList += areasFormatted
//Something like this
self.codeINeedAreasFor(areasList)
}
} else {
print(error)
}
}
}
HOWEVER! If you really feel the need to use some completion handlers, check out this other answer for more info on how to use them. But keep in mind all tools have a time and a place...
There are a few issues here.
Your completion handler doesn't require you to define the name for your completion handler's parameters, so you could easily use completion: (Bool) -> ()
Further in your function, you're returning areaList. This should be put through the completion handler like this onComplete(areaList) and change your completion handler parameter to expect your area list.
Then, when you call your function, it could look more like this :
loadAreasNew { result in
if (result == true) {
print(self.areaList)
} else {
print("Didn't Work")
}
}
Here is my concern:
1) Don't pass in a local variable and make the function return it, it's meaningless and danger.
You may want to initiate an empty array and make your fetch, then "return" it.
2) The fetch request is processed in background, you will have no idea when it will have finished. If you return the array immediately it will always be an empty array.
Put the "return" in your completion too.
3) Parse already has a distance checking method, you don't have to do it manually. aPARSEQUERRY.where(key:,nearGeoPoint:,inKilometers:)
I will rewrite the function as:
func loadNewAreas(completion:([Area],err?)->()){
let areaQuery = PFQuery(className: "Area")
areaQuery.where("location",nearGeoPoint:MYCURRENTLOCATION,inKilometers:50)
areaQuery.findObjectInBackgroundWithBlock(){objects,err
if objects.count == 0
{
completion([],err)
}
let areas = Area.areasFromPFObjects(objects)
completion(areas,err)
}
}
I have such query to parse.com.
Why the numObjects variable has different values inside the findObjectsInBackgroundWithBlock and on the function exits
func searchUserInParse () -> Int {
var numObjects : Int = 0 // the num return objects from query
var query = PFQuery(className:"Bets")
query.whereKey("user", equalTo: "Bob")
query.findObjectsInBackgroundWithBlock {
(objects: AnyObject[]!, error: NSError!) -> Void in
if !error {
numObjects = objects.count
println(numObjects) // at this point the value = 1
} else {
// Log details of the failure
NSLog("Error: %# %#", error, error.userInfo)
}
}
println(numObjects) // at this point the value = 0
return numObjects
}
Instead of using findObjectsInBackgroundWithBlock which runs asynchronously, try using findObjects which runs synchronously:
//Set up query...
var objects = query.findObjects()
numObjects = objects.count
println(numObjects)
Then when running your function, do it like so:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) {
//Search users
searchUserInParse()
dispatch_async(dispatch_get_main_queue()) {
//Show number of objects etc.
}
}
That's the nature of async code, your function will run the outer code to completion, then some time later (depending on connection speed and query complexity) the completion block will run.
Your calling code should do something like the following:
create the query
start the query with a completion block
show a loading animation
return (query results still unknown)
Then you should think about the inside of the block:
check for errors
update values that the UI is bound to
tell the UI to refresh
You can't have a function that will return the count, but you could write a function that takes a completion block as a parameter, and executes it in the query completion block. That's a bit more advanced though.
query.findObjectsInBackgroundWithBlock is will be executed asynchronously , the completion block is called after fetching objects. therefore the code after the block called first hence numObjects value is 0.