Timer is not allowing to add annotation on mapView in iOS swift

130 Views Asked by At

I have a situation where I have to call an API to fetch some Vehicles Locations objects in an array after getting the user current location. After fetching vehicles, I have to get the address also from Vehicles Locations data, so for 'n' Vehicles, there will be an 'n' API call and then add annotations on Map.

After that, I have to refresh the Vehicles data every 1 min. So, I created a timer but even after getting the API response, annotations are not displaying on map. Kindly look into this issue.

Below is Map View

import MapKit


class MapViewController: UIViewController, MKMapViewDelegate {
    
    @IBOutlet private var mapView: MKMapView!
    var currentLocation: CLLocation?
    var user: User?
    lazy var vehicleViewModel = {
        VehicleViewModel()
    }()
    
    var locationUpdateTimer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureLocationManager()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        stopTimer()
    }
    
    func configureLocationManager() {
        LocationManager.shared().delegate = self
        LocationManager.shared().initializeLocationManager()
    }
    
    func configureTimer() {
        if locationUpdateTimer == nil {
            locationUpdateTimer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(runLocationTimer), userInfo: nil, repeats: true)
        }
    }
    
    @objc func runLocationTimer() {
        fetchVehiclesLocation()
    }
    
    func resetMap() {
        let annotations = mapView.annotations
        mapView.removeAnnotations(annotations)
        mapView = nil
    }
    
    func initializeMapView() {
        mapView = MKMapView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height))
        mapView.delegate = self
    }
    
    func configureMapView() {
        let mapDetail = vehicleViewModel.getLatitudeLongitudeLatitudeDeltaLongitudeDelta()
        if let latitude = mapDetail.0, let longitude = mapDetail.1, let latitudeDelta = mapDetail.2, let longitudeDelta = mapDetail.3  {
            
            let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), latitudinalMeters: latitudeDelta, longitudinalMeters: longitudeDelta)
            
            let scaledRegion: MKCoordinateRegion = mapView.regionThatFits(region)
            mapView.setRegion(scaledRegion, animated: true)
            
            mapView.setCameraBoundary(
                MKMapView.CameraBoundary(coordinateRegion: region),
                animated: true)
            
            let zoomRange = MKMapView.CameraZoomRange(maxCenterCoordinateDistance: 100000)
            mapView.setCameraZoomRange(zoomRange, animated: true)
            
            mapView.register(
                VehicleAnnotationView.self,
                forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
        }
    }
    
    func fetchVehiclesLocation() {
        configureTimer()

        initViewModel {
            DispatchQueue.main.async {
                self.resetMap()
                self.initializeMapView()
                self.configureMapView()
            }

            if let user = self.user {
                self.vehicleViewModel.fetchVehicleAddress(user: user, completion: { status in
                    if self.vehicleViewModel.vehicleAnnotationItems.count == 0 {
                        self.alertWithTitleAndMessageWithOK(("Alert" , "error while fetching vehicle locations"))
                    } else {
                        DispatchQueue.main.async {
                            self.mapView.addAnnotations(self.vehicleViewModel.vehicleAnnotationItems)
                        }
                    }
                })
            }
        }
    }
    
    func initViewModel(completion: @escaping () -> Void) {
        if let user = self.user, let userId = user.userId {
            vehicleViewModel.getVehiclesLocation(userId: userId) { (vehicleApiResponse, error) in
                if vehicleApiResponse != nil {
                    completion()
                } else {
                    self.alertWithTitleAndMessageWithOK(("Alert" , error?.localizedDescription ?? "error while fetching vehicles"))
                }
            }
        }
    }
    
    
    func stopTimer()     {
        if locationUpdateTimer != nil {
            locationUpdateTimer!.invalidate()
            locationUpdateTimer = nil
        }
    }
    
    deinit {
        stopTimer()
    }
}

//MARK: - LocationManagerDelegate methods

extension MapViewController: LocationManagerDelegate {
    func didFindCurrentLocation(_ location: CLLocation) {
        currentLocation  = location
        if let currentLocation = currentLocation, (currentLocation.horizontalAccuracy >= 0) {
            mapView.showsUserLocation = true
            fetchVehiclesLocation()
        }
    }
}

LocationManager Extension class

import CoreLocation

protocol LocationManagerDelegate: AnyObject {
    func didFindCurrentLocation(_ location: CLLocation)
    func didFailedToFindCurrentLocationWithError(_ error: NSError?)
    func alertLocationAccessNeeded()
}

/**
 This class acts as a Singleton for getting location manager updates across the application.
 */
class LocationManager: NSObject {
    
    var manager: CLLocationManager!
    
    private static var sharedNetworkManager: LocationManager = {
        let networkManager = LocationManager()
        return networkManager
    }()
    
    private override init() {
        super.init()
        manager = CLLocationManager()
    }
    
    class func shared() -> LocationManager {
        return sharedNetworkManager
    }

    weak var delegate: LocationManagerDelegate?
    
    //Entry point to Location Manager. First the initialization has to be done
    func initializeLocationManager() {
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.distanceFilter = kCLDistanceFilterNone
        manager.delegate = self
        manager.requestWhenInUseAuthorization()
        manager.allowsBackgroundLocationUpdates = false
        startUpdating()
    }
    
    //Start updating locations
    func startUpdating() {
        manager.startUpdatingLocation()
    }
    
    //Check for whether location services are disabled.
    func locationServicesEnabled() -> Bool {
        let isAllowed = CLLocationManager.locationServicesEnabled()
        return isAllowed
    }
   
}

//MARK: - CLLocation Manager delegate methods

extension LocationManager: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

        guard let location = locations.last else {
            return
        }
        manager.stopUpdatingLocation()
        delegate?.didFindCurrentLocation(location)
//        manager.delegate = nil
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        delegate?.didFailedToFindCurrentLocationWithError(error as NSError?)
    }

    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        switch manager.authorizationStatus {
        case .notDetermined:
            self.manager.requestWhenInUseAuthorization()
            break
        case .authorizedWhenInUse, .authorizedAlways:
            if locationServicesEnabled() {
                self.startUpdating()
            }
        case .restricted, .denied:
            delegate?.alertLocationAccessNeeded()

        @unknown default:
            print("Didn't request permission for location access")
        }
    }
    
}
1

There are 1 best solutions below

2
Duncan C On

Your code has a number of problems.

Neither your initializeMapView() function nor your resetMap() function make any sense.

You should add an MKMapView to your Storyboard, then connect it to your mapView outlet, and then don't assign a new value to mapView. Don't set it to nil, and don't replace the map view in the outlet with a brand new map view you create (like you're doing in initializeMapView().) Both of those things will prevent your map from displaying.

You also never create a timer except in your fetchVehiclesLocation() function, which doesn't seem right.

You also don't show how you're setting up your location manager and asking for location updates. (You call a function initializeLocationManager(). I don't believe that is an Apple-provided function. I'm guessing you added it in an extension to the location manager, but you don't show that code.)

You need to ask if the user has granted permission to use the location manager, and trigger a request for permission if not.

Once you have permission to use the location manager, you need to ask it to start updating the user's location.

You don't show any of that code.

Maybe you're doing that in code you didn't show? It also looks like you don't have your CLLocationManagerDelegate methods defined correctly. I don't know of any delegate method didFindCurrentLocation(_:). You will likely need to implement one of the delegate methods locationManager(_:didUpdateLocations:) or locationManager(_:didUpdateTo:from:).

I suggest searching for a tutorial on using the location manager to display the user's location on a map. It's a little involved, and requires some study to set up.