Apple Mapkit calculateETAWithCompletionHandler loop through array - ios

I am making an application where when you click a button it populates a tableview with all of the Users and their estimated ETA from you. I keep running into an issue when I'm looping through my users and getting their location it doesn't make the Array in the same order that its looping through and therefore the times become all mixed up. I've tried several different ways of looping through them but it seems like when it gets the directions.calculateETA section it is delayed and runs at different times. Can anyone help me run through this efficiently so the loop doesn't run out of order?
for user in usersArray {
var userLocations:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: user["location"].latitude, longitude: user["location"].longitude)
ETARequest(userLocations)
}
func ETARequest(destination:CLLocationCoordinate2D) {
let request = MKDirectionsRequest()
if let currentLocation = self.manager.location?.coordinate {
request.source = MKMapItem(placemark: MKPlacemark(coordinate: currentLocation, addressDictionary: nil))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destination, addressDictionary: nil))
request.transportType = .Automobile
let directions = MKDirections(request: request)
directions.calculateETAWithCompletionHandler({ ( response, error) in
if error == nil {
if let interval = response?.expectedTravelTime {
print(self.formatTimeInterval(interval))
self.durationArray.insert(self.formatTimeInterval(interval), atIndex: 0)
if self.durationArray.count == self.allUsers.count {
print(self.durationArray)
self.tableView.reloadData()
}
}
} else {
print("Error Occurred")
}
})
}
}

The issue here is that calculateETAWithCompletionHandler makes a network request. It's an asynchronous function, so you can't assume that all your calls to it will return in the same order you sent them. Here's one solution to this problem:
If you have a user object with an optional duration property, you can refactor your ETARequest method to take one as input:
func ETARequest(destination:CLLocationCoordinate2D, user: UserType) {
//Do stuff...
}
Then, in the completion handler block you pass to calculateETAWithCompletionHandler, you can replace self.durationArray.insert(self.formatTimeInterval(interval), atIndex: 0) with user.duration = self.formatTimeInterval(interval). (You can still have a counter to determine when all requests have finished.)
This way, each user will have a duration associated with it, and you can display them accordingly by looping through your users in the desired order.

Related

Trying to use reverseGeocodeLocation, but completionHandler code is not being executing

The issue is that the code inside the completionHandler block is never run; I used breakpoints and the program would skip over the completion handler block in build mode
Below are two functions used within PinALandPlaceMark, where most of my code is located
func generateRandomWorldLatitude()-> Double{
let latitude = Double.random(in: -33 ..< 60)
return latitude
}
func generateRandomWorldLongitude()-> Double{
let longitude = Double.random(in: -180 ..< 180)
return longitude
}
func PinALandPlaceMark() -> MKAnnotation {
var LandBasedCountryHasYetToBeFound : (Bool, CLLocationDegrees?, CLLocationDegrees?)
LandBasedCountryHasYetToBeFound = (false,nil,nil)
let randomPinLocation = MKPointAnnotation()
repeat{
if LandBasedCountryHasYetToBeFound == (false,nil,nil){
let latitude: CLLocationDegrees = generateRandomWorldLatitude()
let longitude: CLLocationDegrees = generateRandomWorldLongitude()
let randomCoordinate = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(randomCoordinate, completionHandler: { (placemarks, error) -> Void in
if error != nil{print(error)}else{
guard let placemark = placemarks?.first else{return}
//check which placemark property exists, store in the 0 alpha labelForClosure
if let countryExists = placemark.country {
LandBasedCountryHasYetToBeFound = (true,latitude,longitude)
//country = countryExists as String
// viewController.labelForClosure.text = countryExists
print(" Country Exists!: \(countryExists)")
print(" randomCoordinate \(randomCoordinate)")
}
}
})
}
// print("The country found was on land. This statement is \(LandBasedCountryHasYetToBeFound.occursInCountry)")
else{
let coordinatesOfrandomPinLocation = CLLocationCoordinate2D(latitude: LandBasedCountryHasYetToBeFound.1!, longitude: LandBasedCountryHasYetToBeFound.2!)
randomPinLocation.title = ""//LandBasedCountryHasYetToBeFound.countryName
randomPinLocation.coordinate = coordinatesOfrandomPinLocation
// viewController.mapView.addAnnotation(randomPinLocation)
}
}while LandBasedCountryHasYetToBeFound.0 == false
print("randomPin has been returned, now use pin function inside placemark declaration")
return randomPinLocation
}
Your main problem is that your CLGeocoder instance is held in a local variable inside the loop; This means that it will be released before it has completed its task.
You have a couple of other issues too which would cause you problems even if the reverse geo-coding did complete.
The main one is that you are checking for loop termination using a boolean that is set inside the closure; The closure will execute asynchronously, so the loop will have executed many more times before the boolean is set to true in the case where an address is found.
The second problem is related to and made worse by this; reverse geocoding is rate limited. If you submit too many requests too quickly, Apple's servers will simply return an error. Even if you did wait for the first response before submitting a second, your chances of hitting land at random are pretty low, so you will probably hit this limit pretty quickly.
Ignoring the rate limit problem for the moment, you can use a recursive function that accepts a completion handler rather than using a loop and trying to return a value.
var geoCoder = CLGeocoder()
func pinALandPlaceMark(completion: #escaping (Result<MKAnnotation, Error>) -> Void) {
let latitude: CLLocationDegrees = generateRandomWorldLatitude()
let longitude: CLLocationDegrees = generateRandomWorldLongitude()
let randomCoordinate = CLLocation(latitude: latitude, longitude: longitude)
geocoder.reverseGeocodeLocation(randomCoordinate) { (placemarks, error) in
guard error == nil else {
completion(nil,error)
return error
}
if let placemark = placemarks.first, let _ = placemark.country {
let randomPinLocation = MKPointAnnotation()
randomPinLocation.coordinate = randomCoordinate.coordinate
completionHandler(randomPinLocation,nil)
} else {
pinALandPlaceMark(completion:completion)
}
}
}
The first thing we do is declare a property to hold the CLGeocoder instance so that it isn't released.
Next, this code checks to see if a placemark with a country was returned. If not then the function calls itself, passing the same completion handler, to try again. If an error occurs then the completion handler is called, passing the error
To use it, you would say something like this:
pinALandPlaceMark() { result in
switch result {
case .success(let placemark):
print("Found \(placemark)")
case .failure(let error):
print("An error occurred: \(error)")
}
}

Mapkit, how to change annotation coordinates to nearest address?

I have a navigation application I am working on, and one use of it is that it can calculate the average of all the annotations coordinates placed by the user(through a search table, and each annotation is placed when they press a result) and find what you might call a middle point, in between all the annotations. This midpoint, however, only goes by coordinates at the moment, meaning that depending on where the users current annotations are, this mid point could wind up in the middle of a lake or a forest, which is not helpful. I want it to find the nearest address to the coordinates of my middle point, and redirect the annotation to there instead. Here's how the annotation is created:
#IBAction func middleFinderButton(_ sender: Any) {
let totalLatitude = mapView.annotations.reduce(0) { $0 + $1.coordinate.latitude }
let totalLongitude = mapView.annotations.reduce(0) { $0 + $1.coordinate.longitude }
let averageLatitude = totalLatitude/Double(mapView.annotations.count)
let averageLongitude = totalLongitude/Double(mapView.annotations.count)
let centerPoint = MKPointAnnotation()
centerPoint.coordinate.latitude = averageLatitude
centerPoint.coordinate.longitude = averageLongitude
mapView.addAnnotation(centerPoint)
}
How can I get this annotation 'centerPoint' to adjust to the nearest address? Thanks.
I would just use a reverse geocode here returning an MKPlacemark. The documentation suggests that normally just one placemark will be returned by the completion handler, on the main thread, so you can use the result straightaway to update the UI. MKPlacemark conforms to the annotation protocol so you can put it directly on the map:
func resolveAddress(for averageCoordinate: CLLocationCoordinate2D, completion: #escaping (MKPlacemark?) -> () ) {
let geocoder = CLGeocoder()
let averageLocation = CLLocation(latitude: averageCoordinate.latitude, longitude: averageCoordinate.longitude)
geocoder.reverseGeocodeLocation(averageLocation) { (placemarks, error) in
guard error == nil,
let placemark = placemarks?.first
else {
completion(nil)
return
}
completion(MKPlacemark(placemark: placemark))
}
}
#IBAction func middleFinderButton(_ sender: Any) {
// your code to find center annotation
resolveAddress(for: centerPoint.coordinate) { placemark in
if let placemark = placemark {
self.mapView.addAnnotation(placemark)
} else {
self.mapView.addAnnotation(centerCoordinate)
}
}

After deriving the driving distance between two points using MKDirections (Swift 4) how to access the distance value outside the closure? [duplicate]

This question already has answers here:
How do i return coordinates after forward geocoding?
(3 answers)
Wait for completion block of writeImageToSavedPhotosAlbum by semaphore
(1 answer)
Closed 4 years ago.
I have written something like this to calculate the driving distance between 2 points / locations.
Method Implementation:
Class 1:
static func calculateDistance(_ location1 : CLLocationCoordinate2D, location2: CLLocationCoordinate2D, completion: #escaping (_ distance: CLLocationDistance?) -> ()) {
let start = MKMapItem(placemark: MKPlacemark(coordinate: location1))
let destination = MKMapItem(placemark: MKPlacemark(coordinate: location2))
let request = MKDirectionsRequest()
request.source = start
request.destination = destination
request.requestsAlternateRoutes = false
let direction = MKDirections(request: request)
var distanceInMiles: CLLocationDistance?
direction.calculate { (response, error) in
if let response = response, let route = response.routes.first {
distanceInMiles = route.distance * 0.000621371
completion(distanceInMiles)
}
}
}
Usage Question
Class 2:
How do I access the distance value in a different class? For example, I have a parameterized init, where the third parameter "dist" is of type CLLocationDistance. What I am trying to achieve is to access the distance value from the calculateDistance method of Class1
let assigningDistValue = Class1(coordinate: location, secondParam: paramValue, dist:finalDistance!)
I have pretty much read all suggested solutions related to this problem and nothing helped.
You cannot access finalDistance after the closure, because the code runs in this order:
var finalDistance: CLLocationDistance?
// 1:
let calculatedDistance = Class1.calculateDistance(location, location2: secondlocation) { (distance) in
// 3:
guard let distanceInMiles = distance else { return }
print("This is to print distance in miles", distanceInMiles)
finalDistance = calculatedDistance
}
// 2:
let assigningDistValue = Class1(coordinate: location, secondParam: paramValue, dist:finalDistance!)
Just move the let line into the end of the asynchronous material:
let calculatedDistance = Class1.calculateDistance(location, location2: secondlocation) { (distance) in
guard let distanceInMiles = distance else { return }
print("This is to print distance in miles", distanceInMiles)
finalDistance = calculatedDistance
// put it here
let assigningDistValue = Class1(coordinate: location, secondParam: paramValue, dist:finalDistance!) {
// and on we go...
}
}
Or else, use another completion block, just you did in the first code you showed. This is all correct in the first code, but then in the second code all that knowledge of what asynchronous means appears to be forgotten.

MKMapItem.openInMaps() displays place mark exactly 50% of the time

I've found that running some code to display a location in maps view MKMapItem.openInMaps() only works exactly 50% of the time.
In fact it precisely alternates between the MKPlacemark being displayed and it not being displayed.
For example every 1st, 3rd, 5th, 7th ...nth time the code runs then it displays the place mark, but every 2nd, 4th, 6th, 8th ...mth time it runs, the place mark is not displayed.
This is 100% reproducible running the code posted below.
This seems like its a bug, but if so then I'm surprised its not been reported nor fixed previously. But given the fact the failures are precisely alternating between success and failure leads me to think there's something else going on, hence I'm posting here to see if anybody is familiar with this issue or there is something one is supposed to do which is missing from the code, or there is a workaround:
override func viewDidAppear(_ animated: Bool) {
displayMap()
}
func displayMap()
{
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString("1 Infinite Loop, Cupertino,California") { (placemark: [CLPlacemark]?, error: Error?) -> Void in
if error == nil
{
if let placemark = placemark, placemark.count > 0
{
let location = placemark.first
let latitude = (location?.location?.coordinate.latitude)!
let longitude = (location?.location?.coordinate.longitude)!
let coordinates = CLLocationCoordinate2DMake(latitude, longitude)
let regionDistance:CLLocationDistance = 100000
let regionSpan = MKCoordinateRegionMakeWithDistance(coordinates, regionDistance, regionDistance)
let options = [
MKLaunchOptionsMapCenterKey: NSValue(mkCoordinate: regionSpan.center),
MKLaunchOptionsMapSpanKey: NSValue(mkCoordinateSpan: regionSpan.span)
]
let placemark = MKPlacemark(coordinate: coordinates, addressDictionary: nil)
let mapItem = MKMapItem(placemark: placemark)
mapItem.name = "Apple"
mapItem.phoneNumber = "(405) 123-4567"
mapItem.openInMaps(launchOptions: options)
}
}
else
{
assert(false, "Unable to geocode")
}
}
}
This is what the result looks like when the code is run for the first, third, fifth, seventh ... time
And this is the result when the code is run for the second, fourth, sixth, eighth... time
Notice in the failure screenshot that not only is the place mark not displayed on the map but the slide up is also empty.
(Currently observing this on 10.2 but have seen it on other versions too)
Disclaimer
Yes, there seem to be a bug in mapItem.openInMaps, which opens driving directions as an overlay.
As accurately described by #return0:
...I tried your code and the problem lies in the fact that once you have the location centered and showing, if you don't dismiss it and launch the app again it will not show...
More Oddities
The second time around, mapItem.openInMaps does dismiss the driving directions overlay (instead showing the new location)
If the user closes said overlay, Map is no longer confused (*)
(*) A further oddity to that situation is that once that overlay is closed, the location disappears.
Bug? → Workaround!
Force the directions overlay to go away by requesting more than one location. In practice, it means invoking Maps with twice the same location:
MKMapItem.openMaps(with: [mapItem, mapItem], launchOptions: options)
Workaround bonus
Even if the user taps on the anchor and requests driving direction, subsequent invocation to Maps from your application to that or a different location will not leave it hanging. In other words, it works as expected.
Complete Swift 3 Source Code
func displayMap(_ address:String, name:String, tph:String)
{
let geoCoder = CLGeocoder()
geoCoder.geocodeAddressString(address) { (placemark: [CLPlacemark]?, error: Error?) -> Void in
assert(nil == error, "Unable to geocode \(error)")
if error == nil
{
if let placemark = placemark, placemark.count > 0
{
let location = placemark.first
let latitude = (location?.location?.coordinate.latitude)!
let longitude = (location?.location?.coordinate.longitude)!
let coordinates = CLLocationCoordinate2DMake(latitude, longitude)
let regionDistance:CLLocationDistance = 10000
let regionSpan = MKCoordinateRegionMakeWithDistance(coordinates, regionDistance, regionDistance)
let options = [
MKLaunchOptionsMapCenterKey: NSValue(mkCoordinate: regionSpan.center),
MKLaunchOptionsMapSpanKey: NSValue(mkCoordinateSpan: regionSpan.span)
]
let placemark = MKPlacemark(coordinate: coordinates, addressDictionary: nil)
let mapItem = MKMapItem(placemark: placemark)
mapItem.name = name
mapItem.phoneNumber = tph
MKMapItem.openMaps(with: [mapItem, mapItem], launchOptions: options)
} else {
print("Something wrong with \(placemark)")
}
}
}
}
...and invocation
#IBAction func doApple() {
displayMap("1 Infinite Loop, Cupertino, California", name: "Apple", tph: "(408) 996–1010")
}
#IBAction func doMicrosoft() {
displayMap("One Microsoft Way, Redmond, WA", name: "Microsoft", tph: "1-800-MICROSOFT")
}
#IBAction func doIBM() {
displayMap("1 New Orchard Road. Armonk, New York", name: "IBM", tph: "(914) 499-1900")
}
On iOS 10.3.2 this issue still persists for me. However, this workaround is making mapItem not to disappear:
MKMapItem.openMaps(with: [mapItem], launchOptions: options)

How to set properties within a Closure/Block in Swift

I currently have two fields/properties within my view controller. We are using the calculateDirectionsWithCompletionHandler and trying to set my fields to the value of route.distance and route.expectedTravelTime. Here is the code for that:
func calculateDistanceAndEta(locationCoordinate: CLLocationCoordinate2D) {
let currentLocMapItem = MKMapItem.mapItemForCurrentLocation();
let selectedPlacemark = MKPlacemark(coordinate: locationCoordinate, addressDictionary: nil);
let selectedMapItem = MKMapItem(placemark: selectedPlacemark);
let mapItems = [currentLocMapItem, selectedMapItem];
let request: MKDirectionsRequest = MKDirectionsRequest()
request.transportType = MKDirectionsTransportType.Walking;
request.setSource(currentLocMapItem)
request.setDestination(selectedMapItem);
var directions: MKDirections = MKDirections(request: request);
var distsanceLabelTest = ""
var etaLabelTest = ""
directions.calculateDirectionsWithCompletionHandler { (response, error) -> Void in
if (error == nil) {
if (response.routes.count > 0) {
var route: MKRoute = response.routes[0] as! MKRoute;
// route.distance = distance
// route.expectedTravelTime = eta
println("\(route.distance)")
distsanceLabelTest = "\(route.distance)"
etaLabelTest = "\(route.expectedTravelTime)"
}
} else {
println(error)
}
}
println(distsanceLabelTest)
println(etaLabelTest)
self.distanceLabelString = distsanceLabelTest
self.etaLabelString = etaLabelTest
}
However, we can't seem to set any of the variables as it just returns nil. How do we set our class fields to the values of route.distance and route.expectedTravelTime.
we can't seem to set any of the variables as it just returns nil.
The point of providing a completion block is that calculateDirectionsWithCompletionHandler runs asynchronously and executes the completion routine when it's ready. So your distance and expectedTravelTime properties will indeed be unchanged immediately after calculateDistanceAndEta returns because the process started by calculateDirectionsWithCompletionHandler may not have finished by then. Your completion block will be run when it does finish. If you need to take some action when the properties are set, put that code in your completion block.

Resources