2

I'm working on an iOS app that includes a map view. I want the map to automatically center on the user's location when available, and if the user's location is not available or not granted, I want to set a default location.

In my MapViewModel, I have the following code for centering the map on the user's location:

func centerMapOnUserLocation() {
    if let location = locationManager.location {
        let region = MKCoordinateRegion(center: location.coordinate,
                                        span: MKCoordinateSpan(latitudeDelta: 0.21,
                                                               longitudeDelta: 0.21))
        userLocation = region
        map.setRegion(region, animated: true)
    } else {
        // Set default location
        userLocation = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 38.897957,
                                                                          longitude: -77.036560),
                                          span: MKCoordinateSpan(latitudeDelta: 0.21,
                                                                 longitudeDelta: 0.21))
        map.setRegion(userLocation, animated: true)
    }
}

I have a couple of questions:

Am I correctly handling the centering of the map on the user's location and setting a default location if the user's location is not available or not granted? Is there a better approach or any best practices for handling the centering of the map based on user location? Additionally, I want to implement a feature where the map searches for nearby establishments whenever the user moves the map using pan gesture or zooms in/out. Currently, I have the following code in the regionDidChangeAnimated delegate method:

    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
        if !didSetInitialUserZoom {
               didSetInitialUserZoom = true // Marcar que se ha establecido el zoom inicial
               
            let mapCenterLatitude = userLocation.center.latitude
               let mapCenterLongitude = userLocation.center.longitude
               let centerLocation = CLLocation(latitude: mapCenterLatitude, longitude: mapCenterLongitude)
               
               Task {
                   await self.findStores(centerLocation)
               }
               
               return
           }
        
        timer?.invalidate()
        timer = nil
        
        let userCenterLatitude = userLocation.center.latitude
        let userCenterLongitude = userLocation.center.longitude
        let mapCenterLatitude = mapView.region.center.latitude
        let mapCenterLongitude = mapView.region.center.longitude
        
        let latitudeDifference = abs(userCenterLatitude - mapCenterLatitude)
        let longitudeDifference = abs(userCenterLongitude - mapCenterLongitude)
        
        let userSpanLatitudeDelta = userLocation.span.latitudeDelta
        let userSpanLongitudeDelta = userLocation.span.longitudeDelta
        let mapSpanLatitudeDelta = mapView.region.span.latitudeDelta
        let mapSpanLongitudeDelta = mapView.region.span.longitudeDelta
        
        let coordinateThreshold: Double = 0.12 // Umbral para el movimiento de coordenadas
        let spanThreshold: Double = 0.12 // Umbral para el cambio de span

        if (latitudeDifference > coordinateThreshold || longitudeDifference > coordinateThreshold) ||
           (abs(userSpanLatitudeDelta - mapSpanLatitudeDelta) > spanThreshold || abs(userSpanLongitudeDelta - mapSpanLongitudeDelta) > spanThreshold) {
            
            timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in
                guard let self = self else { return }
                
                let centerLocation = CLLocation(latitude: mapCenterLatitude, longitude: mapCenterLongitude)
                Task {
                    await self.findStores(centerLocation)
                }
            }
        }
    }

Is this the correct approach for implementing this feature, or is there a better way to accomplish it?

Finally, I noticed that when the map is initially loaded, it automatically zooms to the user's location. Is there a way to prevent this and instead show the entire map of Latin America by default?

I would appreciate any guidance or suggestions on how to improve these functionalities in my map view implementation. Thank you!

this is my complete code

@MainActor final class MapViewModel: NSObject, ObservableObject {
    
    var customAnnotations = [MapAnnotation]()
    @Published var popTitle = false
    var map = MKMapView(frame: .zero)
    @Published var userLocation: MKCoordinateRegion = .init()
    private(set) var locationManager: CLLocationManager = .init()
    let defaultRegionRadius: CLLocationDistance = 50000
    private var didSetInitialUserZoom: Bool = false
    private var currentUserLocation: CLLocation?
    private var timer: Timer?
    
    override init() {
        super.init()
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.delegate = self
    }
    
    // MARK: - Location Updates
    func requestLocationAuthorization() {
        locationManager.requestWhenInUseAuthorization()
    }
    
    func startUpdatingLocation() {
        locationManager.startUpdatingLocation()
    }
    
    func stopUpdatingLocation() {
        locationManager.stopUpdatingLocation()
    }
    
    func centerMapOnUserLocation() {
        if let location = locationManager.location {
            let region = MKCoordinateRegion(center: location.coordinate,
                                            span: MKCoordinateSpan(latitudeDelta: 0.21,
                                                                   longitudeDelta: 0.21))
            userLocation = region
            map.setRegion(region, animated: true)
        }
    }

    func findStores(_ location: CLLocation) async {
        
        let request = StoreLocatorRequest(coordinates: Coordinates(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude))
        let shopRepository = DIContainer.resolveShopRepo()
        map.removeAnnotations(map.annotations)
        
        do {
            let stores = try await shopRepository.findNearbyStores(request: request)
            stopUpdatingLocation()
            
            if stores.isEmpty {
                customAnnotations = MapAnnotation.mockAnnotations
            } else {
                for store in stores {
                    let storeCoordinate = CLLocationCoordinate2D(latitude: store.coordinates?.latitude ?? 0.0,
                                                                 longitude: store.coordinates?.longitude ?? 0.0)
                    let storeLocation = CLLocation(latitude: storeCoordinate.latitude, longitude: storeCoordinate.longitude)
                    let distance = location.distance(from: storeLocation)
                    if distance <= defaultRegionRadius {
                        let storeAnnotation = MapAnnotation(title: store.title,
                                                            subtitle: nil,
                                                            coordinate: storeCoordinate,
                                                            type: .store)
                        customAnnotations.append(storeAnnotation)
                    }
                }
            }
        } catch {
            Logger.log(message: error.localizedDescription, category: .technical, type: .debug)
        }
        map.addAnnotations(customAnnotations)
        map.showAnnotations(customAnnotations, animated: true)
        stopUpdatingLocation()
    }
    
}

extension MapViewModel: CLLocationManagerDelegate {
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
           guard let userCoordinates = locations.last else { return }
           let centerLocation = CLLocation(latitude: userCoordinates.coordinate.latitude, longitude: userCoordinates.coordinate.longitude)
           Task {
               await self.findStores(centerLocation)
           }
        let region = MKCoordinateRegion(center: centerLocation.coordinate,
                                        span: MKCoordinateSpan(latitudeDelta: 0.21,
                                                               longitudeDelta: 0.21))
        map.setRegion(region, animated: true)
        map.showsUserLocation = true  
       }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        stopUpdatingLocation()
    }
    //     MARK: It would be great to add a couple of alerts in the event that the user removes the location permission on this screen, but it would be a nice to have.
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        
        switch status {
        case .notDetermined:
            Logger.log(message: "not determined location permission", category: .technical, type: .error)
            map.setRegion(userLocation, animated: true)
            stopUpdatingLocation()
        case .restricted, .denied:
            Logger.log(message: "restricted location permission", category: .technical, type: .error)
            userLocation = .init(center: CLLocationCoordinate2D(latitude: 38.897957,
                                                                longitude: -77.036560),
                                 span: MKCoordinateSpan(latitudeDelta: 0.21,
                                                        longitudeDelta: 0.21))
            map.setRegion(userLocation, animated: true)
            stopUpdatingLocation()
        case .authorizedAlways, .authorizedWhenInUse:
            startUpdatingLocation()
            Logger.log(message: "Authorized permission", category: .technical, type: .info)
        @unknown default:
            Logger.log(message: "unknown location permission", category: .technical, type: .error)
        }
        
    }
    
}
extension MapViewModel: MKMapViewDelegate {
    
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        guard let mapAnnotation = annotation as? MapAnnotation else { return nil }
        let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: mapAnnotation.type.rawValue)
        let petcoLogoImageView = UIImageView(image: UIImage(named: "PetcoTextLogo"))
        let rightArrowImageView = UIImageView(image: UIImage(named: "ChevronRight"))
        
        rightArrowImageView.frame = CGRect(x: 0, y: 0, width: 10, height: 15)
        petcoLogoImageView.frame = CGRect(x: 0, y: 0, width: 55, height: 25)
        
        annotationView.canShowCallout = true
        annotationView.image = UIImage(named: mapAnnotation.type.imageName)
        annotationView.leftCalloutAccessoryView = petcoLogoImageView
        annotationView.rightCalloutAccessoryView = rightArrowImageView
        return annotationView
    }
    
    func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
        guard didSetInitialUserZoom == false else { return }
        didSetInitialUserZoom = true
    }
    
    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
        if !didSetInitialUserZoom {
               didSetInitialUserZoom = true // Marcar que se ha establecido el zoom inicial
               
            let mapCenterLatitude = userLocation.center.latitude
               let mapCenterLongitude = userLocation.center.longitude
               let centerLocation = CLLocation(latitude: mapCenterLatitude, longitude: mapCenterLongitude)
               
               Task {
                   await self.findStores(centerLocation)
               }
               
               return
           }
        
        timer?.invalidate()
        timer = nil
        
        let userCenterLatitude = userLocation.center.latitude
        let userCenterLongitude = userLocation.center.longitude
        let mapCenterLatitude = mapView.region.center.latitude
        let mapCenterLongitude = mapView.region.center.longitude
        
        let latitudeDifference = abs(userCenterLatitude - mapCenterLatitude)
        let longitudeDifference = abs(userCenterLongitude - mapCenterLongitude)
        
        let userSpanLatitudeDelta = userLocation.span.latitudeDelta
        let userSpanLongitudeDelta = userLocation.span.longitudeDelta
        let mapSpanLatitudeDelta = mapView.region.span.latitudeDelta
        let mapSpanLongitudeDelta = mapView.region.span.longitudeDelta
        
        let coordinateThreshold: Double = 0.12 // Umbral para el movimiento de coordenadas
        let spanThreshold: Double = 0.12 // Umbral para el cambio de span

        if (latitudeDifference > coordinateThreshold || longitudeDifference > coordinateThreshold) ||
           (abs(userSpanLatitudeDelta - mapSpanLatitudeDelta) > spanThreshold || abs(userSpanLongitudeDelta - mapSpanLongitudeDelta) > spanThreshold) {
            
            timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in
                guard let self = self else { return }
                
                let centerLocation = CLLocation(latitude: mapCenterLatitude, longitude: mapCenterLongitude)
                Task {
                    await self.findStores(centerLocation)
                }
            }
        }
    }

    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
        // Change the annotation view's image when selected
        if view.annotation is MapAnnotation {
            view.image = UIImage(named: "StoreMapPinGreen")
        }
    }
    
    func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
        if let annotation = view.annotation as? MapAnnotation {
            view.image = UIImage(named: annotation.type.imageName)
        }
    }
}

struct MapComponent: UIViewRepresentable {
    
    @StateObject private var viewModel = MapViewModel()
    
    func makeUIView(context: Context) -> MKMapView {
        viewModel.map.accessibilityIdentifier = "MapView"
        viewModel.map.showsUserLocation = true
        return viewModel.map
    }
    
    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.removeAnnotations(mapView.annotations)
        mapView.delegate = viewModel
    }
    
}

A solution in the code

0 Answers0