Threading CompletionHandler Swift - swift

i try to get data from parse server. Therefore it takes 2 backgroundthreads. But i dont get it managed to wait for the completion in a right way. So i have splitted it up like following code:
func load(loadCompletion: #escaping () -> ()) {
let delegate = object as! AppDelegate
parseQuery.findObjectsInBackground { (result: [PFObject]?, error: Error?) in
self.getAllData(result: result, delegate: delegate, error: error) {
loadCompletion()
}
}
}
func getAllData(result: [PFObject]?, delegate: AppDelegate, error: Error?, allDataCompletion: #escaping () -> ()) {
if error == nil && result != nil {
for obj in result! {
let date: Date = obj["Date"] as! Date
let coordinates: PFGeoPoint = obj["Coordinates"] as! PFGeoPoint
let imageFile: PFFileObject = obj["Image"] as! PFFileObject
let lat: CLLocationDegrees = coordinates.latitude
let long: CLLocationDegrees = coordinates.longitude
let cllCoordinates = CLLocationCoordinate2D(latitude: lat, longitude: long)
self.getImageData(imageFile: imageFile) { (image) in
let poo = Poo(coordinates: cllCoordinates, dateTime: date, image: image)
delegate.poos.append(poo)
}
}
allDataCompletion()
}
}
func getImageData(imageFile: PFFileObject, imageDataCompletion: #escaping (UIImage?) -> () ) {
var image: UIImage? = nil
imageFile.getDataInBackground { (data, error) in
if error == nil && data != nil {
image = UIImage(data: data!)
}
imageDataCompletion(image)
}
}
So, i want so set up the array in the delegate, but unfortunately the loadCompletion() gets called before the array is filled. Please help me to get this running in right order. Thanks!

A simple solution is to modify your getAllData function and call allDataCompletion after getting image data for the last object.
func getAllData(result: [PFObject]?, delegate: AppDelegate, error: Error?, allDataCompletion: #escaping () -> ()) {
if error == nil && result != nil {
for (idx, obj) in result!.enumerated() {
let date: Date = obj["Date"] as! Date
let coordinates: PFGeoPoint = obj["Coordinates"] as! PFGeoPoint
let imageFile: PFFileObject = obj["Image"] as! PFFileObject
let lat: CLLocationDegrees = coordinates.latitude
let long: CLLocationDegrees = coordinates.longitude
let cllCoordinates = CLLocationCoordinate2D(latitude: lat, longitude: long)
self.getImageData(imageFile: imageFile) { (image) in
let poo = Poo(coordinates: cllCoordinates, dateTime: date, image: image)
delegate.poos.append(poo)
if idx == result!.endIndex-1{
allDataCompletion()
}
}
}
}
}
or Use DispatchGroup / Synchronizing Async Code

Related

How to properly wait until function has finished doing in swift?

I have now tried lots of things, but none of them seem to work.
I have a for loop which parses some data and converts coordinates into ZIP string:
for i in 0 ... results.count - 1
{
result = results[i]
self.coordinateToString(lat: result.lat, long: result.long, completion: { (place) in
someCell.label.text = place
})
}
func coordinateToString(lat: Double, long: Double, completion: #escaping (String) -> ()) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: lat, longitude: long)
var ret = ""
geoCoder.reverseGeocodeLocation(location, completionHandler:
{
placemarks, error -> Void in
guard let placeMark = placemarks?.first else { return }
if let zip = placeMark.postalCode, let town = placeMark.subAdministrativeArea
{
let toAppend = "\(zip)" + " \(town)"
ret = toAppend
}
})
DispatchQueue.main.async {
completion(ret)
}
}
However I never manage to show the correct place in the cell, it always shows empty space because it somehow doesn't wait for the completion handler to finish converting. What am I doing wrong here?
This happens because reverseGeocodeLocation returns right away and its completion handler runs afterwards. This means that ret value may be empty when it gets put on the main queue. You should dispatch to main from within the callback, like so:
func coordinateToString(lat: Double, long: Double, completion: #escaping (String) -> ()) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: lat, longitude: long)
var ret = ""
geoCoder.reverseGeocodeLocation(location, completionHandler:
{
placemarks, error -> Void in
guard let placeMark = placemarks?.first else { return }
if let zip = placeMark.postalCode, let town = placeMark.subAdministrativeArea
{
let toAppend = "\(zip)" + " \(town)"
ret = toAppend
DispatchQueue.main.async {
completion(ret)
}
}
})
Of course, given this scenario, you need to handle error cases accordingly. Better yet, use defer, that way completion gets called regardless of what happens:
func coordinateToString(lat: Double, long: Double, completion: #escaping (String) -> ()) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: lat, longitude: long)
var ret = ""
geoCoder.reverseGeocodeLocation(location, completionHandler:
{
defer {
DispatchQueue.main.async {
completion(ret)
}
}
placemarks, error -> Void in
guard let placeMark = placemarks?.first else { return }
if let zip = placeMark.postalCode, let town = placeMark.subAdministrativeArea
{
let toAppend = "\(zip)" + " \(town)"
ret = toAppend
}
})

swift 3 Calculate distance to current location and sort result from closet to furthest

I'm trying to to calculate the distance from an event to my current location, sort the results and populate that in a tableview. I keep getting error for optional unwrapped value distance is nil.
private func observeEvents() {
refHandle = ref.observe(.childAdded, with: { (snapshot) -> Void in
let eventDetails = snapshot.value as! Dictionary<String, AnyObject>
let eventID = snapshot.key
let location = eventDetails["location"] as! String!
//calculating distance
self.forwardGeocoding(address: location!)
let distance = self.eventLocation?.distance(from: self.currentLocation!) as Double!
//end calculating
let dateTime = eventDetails["dateTime"] as! String!
let addedByUser = eventDetails["addedByUser"] as! String!
let attending = eventDetails["attendance"] as! String!
if let name = eventDetails["eventName"] as! String! , name.characters.count > 0
{
self.events.append(Events(id:eventID, name: name, location: location!, dateTime: dateTime!, addedByUser: addedByUser!, attending: attending! , distance: distance!))
self.events.sort(by: { $0.distance < $1.distance})
self.tableView.reloadData()
} else {
print("Error ! Can't load events from database")
}
})
} //load events data to uitableview
I created a function to return a CLLocation from an address
func forwardGeocoding(address: String) {
CLGeocoder().geocodeAddressString(address, completionHandler: { (placemarks, error) in
if error != nil {
print(error!)
return
}
if (placemarks?.count)! > 0 {
let placemark = placemarks?[0]
self.eventLocation = placemark?.location
}
})
}
I finally figured out the answer. The issue was the function for distance is called asynchronously there for the result would always be nil. I created a completion handler for the forwardGeocoding function to return latitude and longitude from the address string and call the result inside the nested firebase listener. Here is the code, I hope if someone ran into something similar problem to me will find it helpful.
//Get lat and long
func getCoordinates(address: String, completionHandler: #escaping (_ lat: CLLocationDegrees?, _ long: CLLocationDegrees?, _ error: Error?) -> ()) -> Void {
var _:CLLocationDegrees
var _:CLLocationDegrees
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks: [CLPlacemark]!, error: Error!) in
if error != nil {
print("Geocode failed with error: \(error.localizedDescription)")
} else if placemarks.count > 0 {
let placemark = placemarks[0] as CLPlacemark
let location = placemark.location
let lat = location?.coordinate.latitude
let long = location?.coordinate.longitude
completionHandler(lat, long, nil)
}
}
}
Nested call in firebase listener
refHandle = ref.observe(.childAdded, with: { (snapshot) -> Void in
let location = event["address"] as! String
self.getCoordinates(address: location!) { lat, long, error in
if error != nil {
print("Error")
} else {
self.latitude = lat
self.longitude = long
let distance = CLLocation(latitude: self.latitude!,longitude: self.longitude!).distance(from: self.currentLocation!)
if let name = eventDetails["eventName"] as! String! , name.characters.count > 0
{
self.events.append(Events(id:eventID, name: name, location: location!, dateTime: dateTime!, addedByUser: addedByUser!, attending: attending!, distance: distance))
self.events.sort(by: { $0.distance < $1.distance})
self.tableView.reloadData()
} else {
print("Error ! Can't load events from database")
}
}
}
})

Get Data From Async Completion Handler

Trying to get name of a city, while having latitude and longitude.
Inside a model class Location, I'm using reverseGeocodeLocation(location: , completionHandler: ) func that comes with CLGeocoder (part of CoreLocation).
func getLocationName() {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
})
}
However, in ViewController, after running the getLocationName(), the location.currentCity is nil, since the completion handler is async, and wasn't finished yet.
How can I make sure that the completion handler is finished running so I can access location.currentCity ?
Pass a closure as a function parameter in your getLocationName which
you can call inside the reverseGeocodeLocation closure.
func updateLocation(currentCity : String) -> Void
{
print(currentCity)
}
func getLocationName(callback : #escaping (String) -> Void)
{
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String
{
self.currentCity = city
callback(city)
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
})
}
In your ViewController...
getLocationName(callback: updateLocation)
I would create a function where location.currentCity is used, and call this function from the completion handler
So if your code looks like:
func foo() {
var location
getLocationName()
print(location.currentcity) // nil
}
change it to:
func foo() {
var location
getLocationName()
}
func bar() {
print(location.currentcity) // someplace
}
and call bar() from your completion handler

How To Call a func within a Closure

In a model's class Location, I get the name of the current city:
var currentLatitude: Double!
var currentLongitude: Double!
var currentLocation: String!
var currentCity: String!
func getLocationName() {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
self.nowUpdateUI()
})
}
In view controller I want to update the UI and update my label to show the current city.
However, self.currentCity = city happens inside of a closure. So if I just run a func in view controller:
func updateUI() {
cityLbl.text = Location.sharedInstance.currentCity
}
I'm not getting anywhere because the closure haven't finished running.
I've been advised to add a completion handler to getLocationName() and inside of it, perform the call to a func that will update the UI.
However, from all the tutorials out there on closures, completion handlers, it is not clear to me how to achieve that.
How to construct a completion handler, pass it as an arg to getLocationName() and how to call getLocationName from view controller?
To handle this situation you have multiple option.
Create delegate/protocol with your Location class
Create one protocol and implement that protocol method with your ViewController and declare its instance in your Location class. After then in the completionHandler of reverseGeocodeLocation call this delegate method. Check Apple documentation on Protocol for more details.
You can create completionHandler with your getLocationName method of Location class.
Add completionHandler with getLocationName and called that completionHandler inside the completionHandler of reverseGeocodeLocation like this way.
func getLocationName(completionHandler: #escaping (_ success: Bool) -> Void) {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
completionHandler(false)
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
completionHandler(true)
//self.nowUpdateUI()
})
}
Now in ViewController where you are calling this function call your updateUI method inside the completion block.
Location.sharedInstance.getLocationName { (success) in
if success {//If successfully got response
self.updateUI()
}
}
You can add observer for (NS)NotificationCenter.
Register the observer with (NS)NotificationCenter and then post the notification inside the completionHandler of reverseGeocodeLocation. You can get more detail on this with this StackOverflow Post.
// I thing issue back ground thread you need to update your UI in main thread
var currentLatitude: Double!
var currentLongitude: Double!
var currentLocation: String!
var currentCity: String!
func getLocationName() {
let geoCoder = CLGeocoder()
let location = CLLocation(latitude: currentLatitude, longitude: currentLongitude)
geoCoder.reverseGeocodeLocation(location, completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
DispatchQueue.main.async {
self.nowUpdateUI()
// Update your UI in main thread
}
})
}
This entire piece of your code:
completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
print(city)
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
self.nowUpdateUI()
}
)
is already happening in the completionHandler (which happens after everything is finished) Just also run your updateUI() inside the completionHandler. So your end code would be :
completionHandler: { placemarks, error in
guard let addressDict = placemarks?[0].addressDictionary else {
return
}
if let city = addressDict["City"] as? String {
self.currentCity = city
DispatchQueue.main.async {
updateUI()
}
}
if let zip = addressDict["ZIP"] as? String {
print(zip)
}
if let country = addressDict["Country"] as? String {
print(country)
}
self.nowUpdateUI()
}
)
The reason you have to use DispatchQueue.main is because your completionHandler is on a backgroundqueue but you MUST always do you UI related stuff from your mainQueue—so users get the fastest changing in their UI without any glitches. Imagine if you were doing on a background thread and it was happening slow

download and save a PFFile (UIImage) to show in a UIImageView : SWIFT

I have a large class called "Goal" in parse. This class has multiple elements, one of which is a PFFile, that is always a UIImage.
When I perform my query for the "Goal" class, I cannot figure out how to take the PFFile, and change it to a UIImage for use.
var query = PFQuery(className:"Goal")
let currentUser = PFUser.currentUser()!.username
query.whereKey("creator", equalTo: currentUser!)
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]?, error: NSError?) -> Void in
if error == nil {
// The find succeeded.
println("Successfully retrieved \(objects?.count) goals for the TableView.")
// Do something with the found objects
if let objects = objects as? [PFObject] {
for object in objects {
let goalType = object["type"] as! String
let goalPeriod = object["period"] as! String
let goalCategory = object["category"] as! String
let goalShortDescription = object["shortDescription"] as! String
let goalLongDescription = object["longDescription"] as! String
let goalPointvalue = object["pointValue"] as! Int
let goalSharedSetting = object["shared"] as! Bool
let goalAdoptionCount = object["adoptionCount"] as! Int
let goalIsComplete = object["isComplete"] as! Bool
let goalSuccessImageData = object["image"] as! PFFile
goalSuccessImageData.getDataInBackgroundWithBlock {
(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
if let imageData = imageData {
let image = UIImage(data:imageData)
self.imageQuery = image
}
}
}
let goalSuccessImage : UIImage = self.imageQuery
let goalObjectID = object.objectId
let goalSpreadCount = object["spreadCount"] as! Int
let goalSpreadTotal = object["spreadTotal"] as! Int
let goalTotalCompletions = object["totalCompletions"] as! Int
let thisGoal = GoalModel(period: goalPeriod, type: goalType, category: goalCategory, shortDescription: goalShortDescription, longDescription: goalLongDescription, pointValue: goalPointvalue, shared: goalSharedSetting, adoptionCount: goalAdoptionCount, isComplete: goalIsComplete, successImage: goalSuccessImage, goalID: goalObjectID!, spreadCount: goalSpreadCount, spreadTotal: goalSpreadTotal, totalCompletions: goalTotalCompletions ) as GoalModel
any tips on how to modify the "success image" part? I added a space before and after to make it easier to find.
Thank you in advance!
I'm using this way in my projects, if it help's you :
func performSave(sender: UIBarButtonItem){
affichageActivityIndicator()
let qos = Int(QOS_CLASS_USER_INITIATED.value)
dispatch_async(dispatch_get_global_queue(qos,0)) { () -> Void in
dispatch_async(dispatch_get_main_queue()){
if let updateObject = self.currentObject as PFObject? {
let imageData = UIImageJPEGRepresentation(imageToSave, 0.1)
let imageFile = PFFile(name:"image.png", data:imageData)
updateObject["imageFile"] = imageFile
// Save the data back to the server in a background task
updateObject.saveInBackgroundWithBlock{(success: Bool, error: NSError!) -> Void in
UIApplication.sharedApplication().endIgnoringInteractionEvents()
if success == false {
println("Error")
}
}
}
}
}
}