I am trying to create a LocationManager
class that my ViewModel
will use in a view. I want to be able to use the checkIfLocationServicesIsEnabled()
function in my ViewModel
to be able to get the MKCoordinateRegion
and initialize it to the @Published var region
variable in my ViewModel
. However, the checkIfLocationServicesIsEnabled()
function in my LocationManager
seems to only return a nil
value. I am struggling to understand why this is the case.
My code:
LocationManager
import CoreLocation
import MapKit
import SwiftUI
final class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
static let shared = LocationManager()
private override init() {}
// Optional because user can have location services in phone turned off.
var locationManager: CLLocationManager?
var region: MKCoordinateRegion?
func checkIfLocationServicesIsEnabled() -> MKCoordinateRegion? {
if CLLocationManager.locationServicesEnabled() {
self.locationManager = CLLocationManager()
locationManager!.delegate = self
return region
} else {
print("Show alert letting the user know this is off and to go turn it on.")
return nil
}
}
private func checkLocationAuthorization() {
guard let locationManager else { return }
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestAlwaysAuthorization()
case .restricted:
print("Parental controls prevent us from using your location.")
case .denied:
print("You have denied location permission.")
case .authorizedAlways, .authorizedWhenInUse:
region = MKCoordinateRegion(center: locationManager.location!.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
@unknown default:
break
}
}
// Called when locationManager is created, or when authorization is changed.
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuthorization()
}
}
ViewModel
import CoreLocation
import MapKit
import SwiftUI
final class ViewModel: ObservableObject {
@Published var isSettingLocationManually = false
@Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 25, longitude: 46),
span: MKCoordinateSpan(latitudeDelta: 30, longitudeDelta: 30))
func checkIfLocationServicesIsEnabled() {
if let region = LocationManager.shared.checkIfLocationServicesIsEnabled() {
self.region = region
print("success")
}
print("did it work?")
}
}
The function returns a nil
value. How can I get it to return an MKCoordinateRegion
? Any help would be greatly appreciated.
CodePudding user response:
You've got a couple problems here. You shouldn't be using !
at all. You need to tell the location manager that you want it to start tracking. Even if you did tell it to start tracking
if CLLocationManager.locationServicesEnabled() {
self.locationManager = CLLocationManager()
locationManager!.delegate = self
return region
}
At this time it would not have a region, because it wouldn't have had the chance to start tracking yet.
I just implemented something very similar in one of my apps, dig through this and you can see how it all comes together.
//
// ContentView.swift
//
// Created by Andrew Carter on 12/15/22.
//
import SwiftUI
import CoreLocation
import MapKit
// Don't forget to add NSLocationWhenInUseUsageDescription to your info plist!
class LocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var continuations: [CheckedContinuation<MKCoordinateRegion, Never>] = []
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestWhenInUseAuthorization()
}
func fetchLocation() async -> MKCoordinateRegion {
manager.startUpdatingLocation()
return await withCheckedContinuation { continuation in
self.continuations.append(continuation)
}
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {
return
}
continuations.forEach { continuation in
let region = MKCoordinateRegion(center: location.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
continuation.resume(returning: region)
}
continuations.removeAll()
manager.stopUpdatingLocation()
}
}
@MainActor
class MyLocationProvider: ObservableObject {
@Published var region: MKCoordinateRegion?
private let locationManager = LocationManager()
init() {
Task {
self.region = await locationManager.fetchLocation()
}
}
}
struct ContentView: View {
@StateObject var locationProvider = MyLocationProvider()
var body: some View {
if let region = locationProvider.region {
Text("Region is " String(describing: region))
} else {
Text("Fetching region...")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
We can use async await to make a nicer api. You can even constantly publish updates if you wanted to instead of fetching the location just once. Also note that you need NSLocationWhenInUseUsageDescription
defined in your bundle's info plist!