I had the same problem, and after a few hours, I managed to get something working as required
- At launch, it shows the user location if he authorised it, but waits for him to tap the button to activate the follow.
- If he authorised and taps the button, it follows him.
- If for some reason the app cannot access his location (denied access, restrictions…), it will tell him to change it in Settings and redirect him there.
- If he changes the authorization while running the app, it will change automatically.
- If the map is following him, the button disappears.
- If he drags the map (stop following), the button appears again.
- (The button supports Dark Mode)
I really hope it'll help you, I think I'll put it on GitHub some day. I'll add the link here if I do.
First, I didn't want to recreate the MKMapView every time, so I put it in a class I called MapViewContainer
I don't think it's a good practice, though ♂️
If you don't want to use it, just replace @EnvironmentObject private var mapViewContainer: MapViewContainer
by let mapView = MKMapView(frame: .zero)
in MKMapViewRepresentable
(and change the mapViewContainer.mapView
for mapView
)
import MapKit
class MapViewContainer: ObservableObject {
@Published public private(set) var mapView = MKMapView(frame: .zero)
}
Then, I could create my MapViewRepresentable
import SwiftUI
import MapKit
// MARK: - MKMapViewRepresentable
struct MKMapViewRepresentable: UIViewRepresentable {
var userTrackingMode: Binding<MKUserTrackingMode>
@EnvironmentObject private var mapViewContainer: MapViewContainer
func makeUIView(context: UIViewRepresentableContext<MKMapViewRepresentable>) -> MKMapView {
mapViewContainer.mapView.delegate = context.coordinator
context.coordinator.followUserIfPossible()
return mapViewContainer.mapView
}
func updateUIView(_ mapView: MKMapView, context: UIViewRepresentableContext<MKMapViewRepresentable>) {
if mapView.userTrackingMode != userTrackingMode.wrappedValue {
mapView.setUserTrackingMode(userTrackingMode.wrappedValue, animated: true)
}
}
func makeCoordinator() -> MapViewCoordinator {
let coordinator = MapViewCoordinator(self)
return coordinator
}
// MARK: - Coordinator
class MapViewCoordinator: NSObject, MKMapViewDelegate, CLLocationManagerDelegate {
var control: MKMapViewRepresentable
let locationManager = CLLocationManager()
init(_ control: MKMapViewRepresentable) {
self.control = control
super.init()
setupLocationManager()
}
func setupLocationManager() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.pausesLocationUpdatesAutomatically = true
}
func followUserIfPossible() {
switch CLLocationManager.authorizationStatus() {
case .authorizedAlways, .authorizedWhenInUse:
control.userTrackingMode.wrappedValue = .follow
default:
break
}
}
private func present(_ alert: UIAlertController, animated: Bool = true, completion: (() -> Void)? = nil) {
// UIApplication.shared.keyWindow has been deprecated in iOS 13,
// so you need a little workaround to avoid the compiler warning
// https://stackoverflow.com/a/58031897/10967642
let keyWindow = UIApplication.shared.windows.first { $0.isKeyWindow }
keyWindow?.rootViewController?.present(alert, animated: animated, completion: completion)
}
// MARK: MKMapViewDelegate
func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
#if DEBUG
print("\(type(of: self)).\(#function): userTrackingMode=", terminator: "")
switch mode {
case .follow: print(".follow")
case .followWithHeading: print(".followWithHeading")
case .none: print(".none")
@unknown default: print("@unknown")
}
#endif
if CLLocationManager.locationServicesEnabled() {
switch mode {
case .follow, .followWithHeading:
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
// Possibly due to active restrictions such as parental controls being in place
let alert = UIAlertController(title: "Location Permission Restricted", message: "The app cannot access your location. This is possibly due to active restrictions such as parental controls being in place. Please disable or remove them and enable location permissions in settings.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
// Redirect to Settings app
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert)
DispatchQueue.main.async {
self.control.userTrackingMode.wrappedValue = .none
}
case .denied:
let alert = UIAlertController(title: "Location Permission Denied", message: "Please enable location permissions in settings.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
// Redirect to Settings app
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert)
DispatchQueue.main.async {
self.control.userTrackingMode.wrappedValue = .none
}
default:
DispatchQueue.main.async {
self.control.userTrackingMode.wrappedValue = mode
}
}
default:
DispatchQueue.main.async {
self.control.userTrackingMode.wrappedValue = mode
}
}
} else {
let alert = UIAlertController(title: "Location Services Disabled", message: "Please enable location services in settings.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
// Redirect to Settings app
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert)
DispatchQueue.main.async {
self.control.userTrackingMode.wrappedValue = mode
}
}
}
// MARK: CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
#if DEBUG
print("\(type(of: self)).\(#function): status=", terminator: "")
switch status {
case .notDetermined: print(".notDetermined")
case .restricted: print(".restricted")
case .denied: print(".denied")
case .authorizedAlways: print(".authorizedAlways")
case .authorizedWhenInUse: print(".authorizedWhenInUse")
@unknown default: print("@unknown")
}
#endif
switch status {
case .authorizedAlways, .authorizedWhenInUse:
locationManager.startUpdatingLocation()
control.mapViewContainer.mapView.setUserTrackingMode(control.userTrackingMode.wrappedValue, animated: true)
default:
control.mapViewContainer.mapView.setUserTrackingMode(.none, animated: true)
}
}
}
}
And finally, put it in a SwiftUI View
import SwiftUI
import CoreLocation.CLLocation
import MapKit.MKAnnotationView
import MapKit.MKUserLocation
struct MapView: View {
@State private var userTrackingMode: MKUserTrackingMode = .none
var body: some View {
ZStack {
MKMapViewRepresentable(userTrackingMode: $userTrackingMode)
.environmentObject(MapViewContainer())
.edgesIgnoringSafeArea(.all)
VStack {
if !(userTrackingMode == .follow || userTrackingMode == .followWithHeading) {
HStack {
Spacer()
Button(action: { self.followUser() }) {
Image(systemName: "location.fill")
.modifier(MapButton(backgroundColor: .primary))
}
.padding(.trailing)
}
.padding(.top)
}
Spacer()
}
}
}
private func followUser() {
userTrackingMode = .follow
}
}
fileprivate struct MapButton: ViewModifier {
let backgroundColor: Color
var fontColor: Color = Color(UIColor.systemBackground)
func body(content: Content) -> some View {
content
.padding()
.background(self.backgroundColor.opacity(0.9))
.foregroundColor(self.fontColor)
.font(.title)
.clipShape(Circle())
}
}