Search code examples
iosarraysswift

How to get rid of the 'The API method must be called from the main thread' problem when retrieving data from an API to use them in arrays? Swift


I come to you because I have the following problem:

I work with the 'GoogleMaps' cocoapods and I need to place several markers in a map by using the latitude, longitude and a codeID that I get from an API. I will present you guys 2 cases: the one that works (that uses 3 hard coded arrays mentioned before) and the one that I try to get from the API and that crashes no matter what I do. OK, the first case (the one that works) is this one:

import UIKit
import GoogleMaps

class ViewController: UIViewController {
// MARK: - Constants and variables
let lat: Double = 38.739429 // User's Latitude
let lon: Double = -9.137115 // User's Longitude
let zoom: Float = 15.0

// MARK: - Elements in the storyboard
@IBOutlet weak var googleMap: GMSMapView!

// MARK: - ViewDidLoad()
override func viewDidLoad() {
    super.viewDidLoad()
    googleMapsStuff()
}

// MARK: - Google maps method
func googleMapsStuff() {
    googleMap.delegate = self
    self.googleMap.isMyLocationEnabled = true // User's current position (blue dot on the map)
    let arrayLat: [Double] = [38.739, 38.74, 38.741, 38.732, 38.7325, 38.733]
    let arrayLon: [Double] = [-9.136, -9.135, -9.134, -9.137, -9.1375, -9.138]
    //var arrayCompanyZoneID: [Int] = []
    let camera: GMSCameraPosition = GMSCameraPosition.camera(withLatitude: lat, longitude: lon, zoom: self.zoom)

    googleMap.camera = camera
    
    for index in 0..<arrayLon.count {
        let marker = GMSMarker()
        marker.position = CLLocationCoordinate2D(latitude: arrayLat[index], longitude: arrayLon[index])
        marker.title = "Marker number: \(index)"
        marker.snippet = "Marker's Lat: \(arrayLat[index]), Marker's Lon: \(arrayLon[index])"
        marker.map = self.googleMap
        print("Index: \(index)")
    }
}
}

And as you can see in the image, it all goes smoothly well:

ScreenShotGood

The problem comes in the second case, when I try to fill the empty arrays (which it seems to do) when I connect to an API to get that data. This is the "failure" case:

struct MyInfo: Codable {
let id: String
let name: String
let x: Double // Longitude
let y: Double // Latitude
let licencePlate: String?
let range: Int?
let batteryLevel: Int?
let seats: Int?
let model: String?
let resourceImageId: String?
let pricePerMinuteParking: Int?
let pricePerMinuteDriving: Int?
let realTimeData: Bool?
let engineType: String?
let resourceType: String?
let companyZoneId: Int
let helmets: Int?
let station: Bool?
let availableResources: Int?
let spacesAvailable: Int?
let allowDropoff: Bool?
let bikesAvailable: Int?
}

class ViewController: UIViewController {
// MARK: - Constants and variables
let lat: Double = 38.739429 // User's Latitude
let lon: Double = -9.137115 // User's Longitude
let zoom: Float = 15.0
var arrayLat: [Double] = [] // [38.7395, 38.739, 38.74, 38.741, 38.732, 38.7325, 38.733]
var arrayLon: [Double] = [] // [-9.1365, -9.136, -9.135, -9.134, -9.137, -9.1375, -9.138]
var arrayCompanyZoneID: [Int] = [] // [1, 2, 3, 4, 5, 6, 7]

// MARK: - Elements in the storyboard
@IBOutlet weak var googleMap: GMSMapView!

// MARK: - ViewDidLoad()
override func viewDidLoad() {
    super.viewDidLoad()
    googleMap.delegate = self
    
    self.googleMap.isMyLocationEnabled = true // User's current position (blue dot on the map)
    let camera: GMSCameraPosition = GMSCameraPosition.camera(withLatitude: self.lat, longitude: self.lon, zoom: self.zoom)

    googleMap.camera = camera
    
    guard let urlAPI = URL(string: "https://apidev.meep.me/tripplan/api/v1/routers/lisboa/resources?lowerLeftLatLon=38.711046,-9.160096&upperRightLatLon=38.739429,-9.137115") else { return }
    
    let task = URLSession.shared.dataTask(with: urlAPI) {(data, response, error) in
        if error == nil {
            guard let urlContent = data else { return }
            
            do {
                let JSONResult = try JSONDecoder().decode([MyInfo].self, from: urlContent) //JSONSerialization.jsonObject(with: urlContent, options: .mutableContainers)
                print("JSON Result:", JSONResult)
                
                for jsonData in JSONResult {
                    self.arrayLon.append(jsonData.x)
                    self.arrayLat.append(jsonData.y)
                    self.arrayCompanyZoneID.append(jsonData.companyZoneId)
                }
                
                print("-----------------")
                print(type(of: JSONResult))
                print("-----------------")
                print("ArrayLon:", self.arrayLon)
                print("ArrayLat:", self.arrayLat)
                print("companyZoneId: ", self.arrayCompanyZoneID)
                print("Count zoneid: ", self.arrayCompanyZoneID.count)
                print("-----------------")
                
                // MARK: - Place the multiple markers on the map
                for index in 0..<self.arrayCompanyZoneID.count {
                    let marker = GMSMarker()
                    marker.position = CLLocationCoordinate2D(latitude: self.arrayLat[index], longitude: self.arrayLon[index])
                    marker.title = "Marker number: \(index)"
                    marker.snippet = "Marker's Lat: \(self.arrayLat[index]), Marker's Lon: \(self.arrayLon[index])"
                    marker.map = self.googleMap
                    print("Index: \(index)")
                }
                
            } catch {
                print("JSON processing failed.")
            }
        } else {
            print("Error serializing JSON:", error!)
        }
    }
    
    task.resume()
}

And it doesn't matter what I do, the console always says:

"Terminating app due to uncaught exception 'GMSThreadException', reason: 'The API method must be called from the main thread' "

I also tried using the method

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])

but it also says that the API method most be called from the main thread.

I'm so stuck here and I invested several hours to this issue but It only fails over and over.

I appreciate your advice and wisdom.


Solution

  • You need

    DispatchQueue.main.async {
    
                // MARK: - Place the multiple markers on the map
                for index in 0..<self.arrayCompanyZoneID.count {
                    let marker = GMSMarker()
                    marker.position = CLLocationCoordinate2D(latitude: self.arrayLat[index], longitude: self.arrayLon[index])
                    marker.title = "Marker number: \(index)"
                    marker.snippet = "Marker's Lat: \(self.arrayLat[index]), Marker's Lon: \(self.arrayLon[index])"
                    marker.map = self.googleMap
                    print("Index: \(index)")
                }
     }
    

    As URLSession.shared.dataTask callback is in a background thread