Misael Landeros
Misael Landeros

Reputation: 595

Update MapView with current location on SwiftUI

Trying to update the mapview of the Project 14 of 100daysOfSwiftUI to show my current location, the problem i can´t zoom in move around

i have this code i add @Binding var currentLocation : CLLocationCoordinate2D and view.setCenter(currentLocation, animated: true) to my MapView so i have a button that send thats value and the view actually move so slow to the location but then i can move away anymore

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {

    @Binding var centerCoordinate: CLLocationCoordinate2D
    @Binding var selectedPlace: MKPointAnnotation?
    @Binding var showingPlaceDetails: Bool
    @Binding var currentLocation : CLLocationCoordinate2D

    var annotations: [MKPointAnnotation]

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context) {

        if annotations.count != view.annotations.count {
            view.removeAnnotations(view.annotations)
            view.addAnnotations(annotations)
        }

        view.setCenter(currentLocation, animated: true)

    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

 class Coordinator: NSObject, MKMapViewDelegate{

    var parent: MapView
    init(_ parent: MapView) {
        self.parent = parent
    }

    func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
         parent.centerCoordinate = mapView.centerCoordinate
     }

     func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
         let identifier = "PlaceMark"
         var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
         if annotationView == nil {
             annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
             annotationView?.canShowCallout = true
             annotationView?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)

         } else {
             annotationView?.annotation = annotation
         }

         return annotationView
     }

     func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
         guard let placemark = view.annotation as? MKPointAnnotation else {return}
         parent.selectedPlace = placemark
         parent.showingPlaceDetails = true

     }

    }
}

an this is my swiftUI view

...
    @State private var currentLocation = CLLocationCoordinate2D()

    var body: some View {
        ZStack{

            MapView(centerCoordinate: $centerCoordinate, selectedPlace: $selectedPlace, showingPlaceDetails: $showingPlaceDetails, currentLocation: $currentLocation ,  annotations: locations)
           // MapView(centerCoordinate: $centerCoordinate, selectedPlace: $selectedPlace, showingPlaceDetails: $showingPlaceDetails, annotations: locations)
                .edgesIgnoringSafeArea(.all)
            VStack{
                Spacer()
                HStack{
                    Spacer()
                    Button(action: {
                        self.getCurrentLocation()
                    }){
                        ButtonIcon(icon: "location.fill")
                    }
                }
                .padding()
            }
        }
        .onAppear(perform: getCurrentLocation)
    }

    func getCurrentLocation() {

        let lat = locationManager.lastLocation?.coordinate.latitude ?? 0
        let log = locationManager.lastLocation?.coordinate.longitude ?? 0

        self.currentLocation.latitude = lat
        self.currentLocation.longitude = log

    }
    ...

UPDATE

thanks for the support I using this class to call locationManager.requestWhenInUseAuthorization()

import Foundation
import CoreLocation
import Combine

class LocationManager: NSObject, ObservableObject {

    override init() {
        super.init()
        self.locationManager.delegate = self
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
        self.locationManager.requestWhenInUseAuthorization()
        self.locationManager.startUpdatingLocation()
    }

    @Published var locationStatus: CLAuthorizationStatus? {
        willSet {
            objectWillChange.send()
        }
    }

    @Published var lastLocation: CLLocation? {
        willSet {
            objectWillChange.send()
        }
    }

    var statusString: String {
        guard let status = locationStatus else {
            return "unknown"
        }

        switch status {
        case .notDetermined: return "notDetermined"
        case .authorizedWhenInUse: return "authorizedWhenInUse"
        case .authorizedAlways: return "authorizedAlways"
        case .restricted: return "restricted"
        case .denied: return "denied"
        default: return "unknown"
        }

    }

    let objectWillChange = PassthroughSubject<Void, Never>()

    private let locationManager = CLLocationManager()
}

extension LocationManager: CLLocationManagerDelegate {

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        self.locationStatus = status
        print(#function, statusString)
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        self.lastLocation = location
        print(#function, location)
    }

}

i just want to center my mapview on my current location when i press the button

Upvotes: 0

Views: 2994

Answers (1)

Rob
Rob

Reputation: 437372

No where here do you ever call locationManager.requestWhenInUseAuthorization(). When I did that (of course, making sure the Info.plist had an entry for NSLocationWhenInUseUsageDescription), it updated the location correctly.

E.g.

func getCurrentLocation() {
    if CLLocationManager.authorizationStatus() == .notDetermined {
        locationManager.requestWhenInUseAuthorization()
    }
    if let coordinate = locationManager.location?.coordinate {
        currentLocation = coordinate
    }
}

Now, this is just a quick and dirty fix to demonstrate that it works. But it’s not quite right, because the first time you call getCurrentLocation, if it has to ask the user for permission, which it does asynchronously, which means that it won’t yet have a location when you get to the lastLocation line in your implementation. This is a one time thing, but still, it’s not acceptable. You’d want your CLLocationManagerDelegate update currentLocation if needed. But hopefully you’ve got enough here to diagnose why your location is not being captured correctly by the CLLocationManager.


FWIW, you might consider using a userTrackingMode of .follow, which obviates the need for all of this manual location manager and currentLocation stuff. The one caveat I’ll mention (because I spent hours one day trying to diagnose this curious behavior), is that the userTrackingMode doesn’t work if you initialize your map view with:

let mapView = MKMapView()

But it works if you do give it some frame, e.g.:

let mapView = MKMapView(frame: UIScreen.main.bounds)

So, for user tracking mode:

struct MapView: UIViewRepresentable {
    @Binding var userTrackingMode: MKUserTrackingMode

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView(frame: UIScreen.main.bounds)
        mapView.delegate = context.coordinator
        mapView.userTrackingMode = userTrackingMode

        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context) {
        view.userTrackingMode = userTrackingMode
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView

        init(_ parent: MapView) {
            self.parent = parent
        }

        // MARK: - MKMapViewDelegate

        func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
            DispatchQueue.main.async {
                self.parent.$userTrackingMode.wrappedValue = mode
            }
        }

        // note, implementation of `mapView(_:viewFor:)` is generally not needed if we register annotation view class
    }
}

And then, we can have a “follow” button that appears when user tracking is turned off (so that you can turn it back on):

struct ContentView: View {
    @State var userTrackingMode: MKUserTrackingMode = .follow

    private var locationManager = CLLocationManager()

    var body: some View {
        ZStack {
            MapView(userTrackingMode: $userTrackingMode)
                .edgesIgnoringSafeArea(.all)

            VStack {
                HStack {
                    Spacer()

                    if self.userTrackingMode == .none {
                        Button(action: {
                            self.userTrackingMode = .follow
                        }) {
                            Text("Follow")
                        }.padding()
                    }
                }

                Spacer()
            }
        }.onAppear { self.requestAuthorization() }
    }

    func requestAuthorization() {
        if CLLocationManager.authorizationStatus() == .notDetermined {
            locationManager.requestWhenInUseAuthorization()
        }
    }
}

Upvotes: 2

Related Questions