Darrell
Darrell

Reputation: 635

SwiftUI mapkit set region to user's current location

I am using Xcode 12.

I am able to show a map with a region, but with hard-coded values.

Instead, I want to set the region of the map based on the user's current location.

I have a LocationManager class, which gets the user's location and publish it.

I have a ShowMapView SwiftUI View that observes an object based on the LocationManager class to get the user's location. But, I don't know how to use the data from the locationManager object to set the region used by the map.

Here is the LocationManager class, which gets the user's location and publishes it.

import Foundation
import MapKit

final class LocationManager: NSObject, ObservableObject {
  @Published var location: CLLocation?
  
  private let locationManager = CLLocationManager()
   
  override init() {
    super.init()
    
    self.locationManager.delegate = self
    
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
    self.locationManager.distanceFilter = kCLDistanceFilterNone
    self.locationManager.requestWhenInUseAuthorization()
    self.locationManager.startUpdatingLocation()
  }
}

extension LocationManager: CLLocationManagerDelegate {
  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    if let location = locations.last {
      self.location = location
    }
  }
}

Here is the ShowMapView SwiftUI View, which needs to get the user's location that's published and set the region used by the map. As you can see, the values are hard-coded for now.

import Combine
import MapKit
import SwiftUI

struct AnnotationItem: Identifiable {
    let id = UUID()
    let name: String
    let coordinate: CLLocationCoordinate2D
}

struct ShowMapView: View {
  @ObservedObject private var locationManager = LocationManager()
  
  @State private var region = MKCoordinateRegion(
    center: CLLocationCoordinate2D(latitude: 38.898150, longitude: -77.034340),
    span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
  )

  var body: some View {
            Map(coordinateRegion: $region, annotationItems: [AnnotationItem(name: "Home", coordinate: CLLocationCoordinate2D(latitude: self.locationManager.location!.coordinate.latitude, longitude: self.locationManager.location!.coordinate.longitude))]) {
               MapPin(coordinate: $0.coordinate)
            }
            .frame(height: 300)
  }
}

Upvotes: 2

Views: 3857

Answers (1)

jnpdx
jnpdx

Reputation: 52312

Here's one possible solution to this:

final class LocationManager: NSObject, ObservableObject {
    @Published var location: CLLocation?
    @Published var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 38.898150, longitude: -77.034340),
        span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
    )
    private var hasSetRegion = false
    
    private let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        
        self.locationManager.delegate = self
        
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
        self.locationManager.distanceFilter = kCLDistanceFilterNone
        self.locationManager.requestWhenInUseAuthorization()
        self.locationManager.startUpdatingLocation()
    }
}

extension LocationManager: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            self.location = location
            
            if !hasSetRegion {
                self.region = MKCoordinateRegion(center: location.coordinate,
                                                 span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
                hasSetRegion = true
            }
        }
    }
}

struct ShowMapView: View {
    @ObservedObject private var locationManager = LocationManager()
    
    var homeLocation : [AnnotationItem] {
        guard let location = locationManager.location?.coordinate else {
            return []
        }
        return [.init(name: "Home", coordinate: location)]
    }
    
    var body: some View {
        Map(coordinateRegion: $locationManager.region, annotationItems: homeLocation) {
            MapPin(coordinate: $0.coordinate)
        }
        .frame(height: 300)
    }
}

In this solution, the region is published by the location manager. As soon as a location is received, the region is centered on that spot (in didUpdateLocations). Then, a boolean flag is set saying the region has been centered initially. After that boolean is set, it no longer updates the region. This will let the user still drag/zoom, etc.

I also changed your code for putting down the pin a little bit. You were force-unwrapping location, which is nil until the first location is set by the location manager, causing a crash. In my edit, it just returns an empty array of annotation items if there isn't a location yet.

Upvotes: 8

Related Questions