Understanding Swift Thread Execution

Greetings,


I'm a long time programmer, but totally new to Swift. I have some experience with Objective-C, but most of my historical training and work has been done in procedural languages (C, Fortran, Pascal, etc.).


I've written a simple application on MacOS (Xcode 10.2, Mojave) to load a set of addresses from a file and plot them as annotations on a MapKit view. This is functionality that I wish were in the standard Maps app but, alas, it is not. It seemed rather simple, so I created a basic Map application (MyMaps) that has "Load Annotations..." and "Clear Annotations" functionality. Almost everything works exactly as I want -- I can load multiple files of annotations, and clear them successfully. But my problem is the SEQUENCE of execution... At the end of loading a set of annotations, I'd like to center the map on the "center of gravity" of the annotations, and have the zoom level encompass the collection of annotations. I think I know all the code to do this -- but it does not seem to be getting executed in the order I would expect -- hence, I'm getting unexpected results. The code is:

//

// MyAnnotations.swift

// MyMaps

//

// Created by Scott Hurd on 2019-07-03.

// Copyright © 2019 Scott Hurd. All rights reserved.

//


import Cocoa

import Foundation

import MapKit



var arrayOfStrings: [String]?



extension ViewController {

func createAnnotations(mapView: MKMapView!) {

let dialog = NSOpenPanel();

dialog.title = "Choose a file";

dialog.showsResizeIndicator = true;

dialog.showsHiddenFiles = false;

dialog.canChooseDirectories = true;

dialog.canCreateDirectories = true;

dialog.allowsMultipleSelection = false;

// dialog.allowedFileTypes = ["txt"];

if (dialog.runModal() == NSApplication.ModalResponse.OK) {

let result = dialog.url // Pathname of the file

if (result != nil) {

let path = result!.path

print("File Selected: ",path,"\n")

// var arrayOfStrings: [String]?

do {

let data = try String(contentsOfFile:path, encoding: String.Encoding.utf8)

arrayOfStrings = data.components(separatedBy: "\n")

print(arrayOfStrings ?? "")

} catch let err as NSError {

// do something with Error

print(err)

}

// filename_field.stringValue = path

}

} else {

// User clicked on "Cancel"

return

}

print("Starting to Process File")


var finalCenter: CLLocationCoordinate2D

var finalCenterSet: Bool = false

var rateCount: Int

rateCount = 0

finalCenter = CLLocationCoordinate2DMake(0, 0)

for loc in arrayOfStrings ?? [""] {

print ("Geocoder call: ",loc)

let geocoder = CLGeocoder()

geocoder.geocodeAddressString(loc) { [weak self] placemarks, error in

if let placemark = placemarks?.first, let location = placemark.location {

let mark = MKPlacemark(placemark: placemark)

if (!finalCenterSet) {

finalCenter = location.coordinate

finalCenterSet = true

print ("Set final Center: ",finalCenter)

}

self?.mapView.addAnnotation(mark)

print("Annotation added")

} else

{

print ("Error ",error as Any," in Geocoding for: ",loc)

}

}

}

print ("Setting Region: Center at: ",finalCenter)

let coordRegion = MKCoordinateRegion(center: finalCenter,latitudinalMeters: 10000,longitudinalMeters: 10000)

self.mapView.setRegion(coordRegion, animated: true)

}

}


I would expect this to:

- open the file dialog to load the strings from the file (this works)

- use a FOR loop to go through the strings, calling CLGeocoder() for each (this seems to work)

- for each Geocoder call, if it is successful, create a placemark and annotate the map (this seems to work)

- if there is an error, don't add a mark, and print the error (this seems to work)

- in the FOR loop, I'm trying to retain the FIRST coordinates that get annotated using a simple flag and assignment

- AFTER the FOR loop, I want to set the Region Center and radius


All the annotations show up correctly on the map, and any errors are reported to the console (and are verifiable by checking them in the actual Maps application, where they also report an error). But the sequence of log entries in the console is:


"Starting to process file"

"Geocoder call" (repeated for each call)

"Setting Region: Center at: CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)

"Set final Center: CLLocationCoordinate2D(latitude: -33.7199, longitude: 150.991)

"Annotation added" (repeated for each annotation)


This is what is puzzling me. How can I be seeing the "Geocoder call" for each entry in the FOR loop in order, but NOT the "Annotation added" confirmation at the same time?


Somehow, the order of execution seems to be that all the Geocoder calls are made, THEN the Region Center gets set (which is at the BOTTOM of the code, AFTER the FOR loop), and THEN the "final Center" gets set (which should be happening on the FIRST annotation being made), followed by ALL the "Annotation added" indicators.


So, I'm very confused as to this order of execution. I'm sorry if this is a very basic question -- but can anyone help me understand the flow here?


Many, many thanks!

Scott

That's because a lot of calls may happen asynchronously (this is different from a pure procedural language).


In this case:


            geocoder.geocodeAddressString(loc) { [weak self] placemarks, error in
          // ………
                    print("Annotation added")
          //     …………
     }


The closure (code between brackets) is called on another thread.

So, the call to

print ("Geocoder call: ",loc)

occurs

then, the closure is executed in another thread and the main code continues immediately, without waiting for the closure to complete.


However, all the calls to print("Annotation added") are probably in the right oprder (should print

print("Annotation added", loaction)


To check this.


How to solve this: a classical way is to use semaphores.


Have a look here to understand this (pretty technical)

h ttps://www.raywenderlich.com/5370-grand-central-dispatch-tutorial-for-swift-4-part-1-2

h ttps://www.raywenderlich.com/5371-grand-central-dispatch-tutorial-for-swift-4-part-2-2

As already explained, `geocodeAddressString` and its closure will be called asynchronously.

This sort of thing may happen even in C, when you pass callback to some asynchronous call.


If you want to execute the callback closure strictly in order, you may need to start the next aync call after the current is finished.

That can be a little bit tricky (for person who are not accustomed to it) and take more time to finish all, so please consider if you really need it.


If you just want to do something after all async calls are finished, you can use DispatchGroup.

        let group = DispatchGroup() //<-
        for loc in arrayOfStrings ?? [""] {
            print("Geocoder call:", loc)
            let geocoder = CLGeocoder()
            group.enter() //Call `enter()` before invoking each async call
            geocoder.geocodeAddressString(loc) { placemarks, error in
                //All UI manipulation should be done in the main thread.
                DispatchQueue.main.async {
                    if let placemark = placemarks?.first, let location = placemark.location {
                        let mark = MKPlacemark(placemark: placemark)
                        if !finalCenterSet {
                            finalCenter = location.coordinate
                            finalCenterSet = true
                            print("Set final Center:", finalCenter)
                        }
                        self.mapView.addAnnotation(mark)
                        print("Annotation added")
                    } else {
                        print ("Error \(error as Any) in Geocoding for: \(loc)")
                    }
                    group.leave() //Call `leave()` after each async call finished
                }
            }
        }
        group.notify(queue: .main) {
            //When the last `leave()` call matching the last `enter()`, this closure is called
            print("Setting Region: Center at:", finalCenter)
            let coordRegion = MKCoordinateRegion(center: finalCenter, latitudinalMeters: 10000, longitudinalMeters: 10000)
            self.mapView.setRegion(coordRegion, animated: true)
        }

Please try.

Understanding Swift Thread Execution
 
 
Q