Search code examples
iosswiftgeojsonmkannotationurlsession

Data from GeoJSON URL end point not appearing on Map using MKAnnotations


I am creating my first IOS app and am not a developer and am really stuck with Map Annotations.

I am trying to get Fire data from a GeoJSON URL end point and display the fires as Annotations on a Map using URLSession and a custom MKAnnotationView and custom fire Pins.

The problem is the Annotations with the GeoJSON Fire data from the URL end point are not appearing on the Map, although data is being returned by the URL session. However, if I manually create a single Fire annotation it is appearing correctly on the map with the custom pin.

Any help would be immensely appreciated, I have spent days trying to figure this out :(

Here is the ViewController.Swift file

import UIKit
import MapKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {

    @IBOutlet weak var mapView: MKMapView!
    var locationManager:CLLocationManager!
    var lat = Double()
    var lon = Double()
    var fires: [Fire] = []
    
    override func viewDidLoad() {

        super.viewDidLoad()
     
        mapView.delegate = self
        mapView.register(FireMarkerView.self,forAnnotationViewWithReuseIdentifier:
            MKMapViewDefaultAnnotationViewReuseIdentifier)
          
        if let url = URL(string: "https://services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/Active_Fires/FeatureServer/0/query?outFields=*&where=1%3D1&f=geojson") {
              
            URLSession.shared.dataTask(with: url) {data, response, error in
                    if let data = data {
                       do {
                           let features = try MKGeoJSONDecoder().decode(data)
                               .compactMap { $0 as? MKGeoJSONFeature }
                             let validWorks = features.compactMap(Fire.init)
                        self.fires.append(contentsOf: validWorks)
                        print([self.fires])
                        
                                 }
                       catch let error {
                                    print(error)
                                   
                       }
                     }
                 }.resume()
           }
        
        
//This code works an annotation appears correctly on map
        /* let fire = Fire(
        title: "Ford Fire",
        incidentShortDescription: "Hwy 35",
        incidentTypeCategory: "WF",
        coordinate: CLLocationCoordinate2D(latitude: 37.7993, longitude: -122.1947))
       
        mapView.addAnnotation(fire)*/
        
        mapView.addAnnotations(fires)
    }
        
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        determineMyCurrentLocation()
    }
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {

    switch manager.authorizationStatus {
        case .authorizedAlways , .authorizedWhenInUse:
            mapView.showsUserLocation = true
            followUserLocation()
            locationManager.startUpdatingLocation()
            break
        case .notDetermined , .denied , .restricted:
            locationManager.requestWhenInUseAuthorization()
            break
        default:
            break
    }
    
    switch manager.accuracyAuthorization {
        case .fullAccuracy:
            break
        case .reducedAccuracy:
            break
        default:
            break
    }
}
func followUserLocation() {
    if let location = locationManager.location?.coordinate {
        let region = MKCoordinateRegion.init(center: location, latitudinalMeters: 4000, longitudinalMeters: 4000)
        mapView.setRegion(region, animated: true)
    }
}

    func determineMyCurrentLocation() {
        locationManager = CLLocationManager()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestAlwaysAuthorization()
        
        if CLLocationManager.locationServicesEnabled() {
            locationManager.startUpdatingLocation()
            //locationManager.startUpdatingHeading()
        }
    }
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        
        let userLocation = locations.first! as CLLocation
        lat = userLocation.coordinate.latitude
        lon = userLocation.coordinate.longitude
        
        let region = MKCoordinateRegion.init(center: location.coordinate, latitudinalMeters: 400000, longitudinalMeters: 400000)
        self.mapView.setRegion(region, animated: true)

        // Call stopUpdatingLocation() to stop listening for location updates,
        // other wise this function will be called every time when user location changes.
        // Need a solution for this.
                manager.stopUpdatingLocation()
        
        print("user latitude = \(userLocation.coordinate.latitude)")
        print("user longitude = \(userLocation.coordinate.longitude)")
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error)
    {
        print("Error \(error)")
    }
}

Here is the Model Class, Fire.swift

import Foundation
import MapKit

class Fire: NSObject, MKAnnotation {
  let title: String?
  let incidentShortDescription: String?
  let incidentTypeCategory: String?
  let coordinate: CLLocationCoordinate2D

  init(
    title: String?,
    incidentShortDescription: String?,
    incidentTypeCategory: String?,
    coordinate: CLLocationCoordinate2D
  ) {
    self.title = title
    self.incidentShortDescription = incidentShortDescription
    self.incidentTypeCategory = incidentTypeCategory
    self.coordinate = coordinate

    super.init()
  }




init?(feature: MKGeoJSONFeature) {
  // 1
  guard
    let point = feature.geometry.first as? MKPointAnnotation,
    let propertiesData = feature.properties,
    let json = try? JSONSerialization.jsonObject(with: propertiesData),
    let properties = json as? [String: Any]
    else {
      return nil
  }

  // 3
    
  title = properties ["IncidentName"] as? String
  incidentShortDescription = properties["IncidentShortDescription"] as? String
  incidentTypeCategory = properties["IncidentTypeCategory"] as? String
  coordinate = point.coordinate
  super.init()
}
    var subtitle: String? {
        return (incidentTypeCategory)
    }
    
    
    var image: UIImage {
      guard let name = incidentTypeCategory else {
        return #imageLiteral(resourceName: "RedFlame")
      }

      switch name {
      case "RX":
        return #imageLiteral(resourceName: "YellowFlame")
      default:
        return #imageLiteral(resourceName: "RedFlame")
      }
    }

Here is the custom MKAnnotation Class: FileMarkerView.swift

import Foundation
import MapKit

class FireMarkerView: MKAnnotationView {
  override var annotation: MKAnnotation? {
    willSet {
      guard let fire = newValue as? Fire else {
        return
      }

      canShowCallout = true
      calloutOffset = CGPoint(x: -5, y: 5)
      rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

      image = fire.image
    }
  }
}

Solution

  • URLSession.shared.dataTask is an asynchronous task, meaning it calls its callback function at some indeterminate time in the future. Code executed outside of its callback (the { }) will end up getting called before the data task has actually completed. Right now, you're setting the annotations outside of that callback.

    To solve this, you need to set the annotations inside of that callback function. So, where you have print([self.fires]), you can do:

    DispatchQueue.main.async { 
      self.mapView.addAnnotations(self.fires)
    } 
    

    The DispatchQueue.main.async is to make sure that an update to the UI gets called on the main thread (the URL task may return on a different thread).