I've prepared in my project the following class to retrieve the user current location:
LocationManager.swift
import Foundation
import CoreLocation
class LocationManager: NSObject {
// - Private
private let locationManager = CLLocationManager()
// - API
public var exposedLocation: CLLocation? {
return self.locationManager.location
}
override init() {
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
}
}
// MARK: - Core Location Delegate
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager,
didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .notDetermined : print("notDetermined") // location permission not asked for yet
case .authorizedWhenInUse : print("authorizedWhenInUse") // location authorized
case .authorizedAlways : print("authorizedAlways") // location authorized
case .restricted : print("restricted") // TODO: handle
case .denied : print("denied") // TODO: handle
default : print("unknown") // TODO: handle
}
}
}
// MARK: - Get Placemark
extension LocationManager {
func getPlace(for location: CLLocation,
completion: @escaping (CLPlacemark?) -> Void) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
guard error == nil else {
print("*** Error in \(#function): \ (error!.localizedDescription)")
completion(nil)
return
}
guard let placemark = placemarks?[0] else {
print("*** Error in \(#function): placemark is nil")
completion(nil)
return
}
completion(placemark)
}
}
}
But I'm not sure how to use it, while using SwiftUI, from my ContentView file. How am I supposed to get the exposedLocation without using the approach I would have used in a standard ViewController (in this case the use of guard, let and return of course generates all kind of errors, since I'm not supposed to use returns in this context, if I got it right). Any hint about how to achieve this? I would like to get the user location whenever a button is pressed (at the moment I've used just mockup data).
ContentView.swift
import SwiftUI
struct Location: Identifiable {
// When conforming to the protocol Identifiable we have to to implement a variable called id however this variable does not have to be an Int. The protocol only requires that the type of the variable id is actually Hashable.
// Note: Int, Double, String and a lot more types are Hashable
let id: Int
let country: String
let state: String
let town: String
}
struct ContentView: View {
// let’s make our variable a @State variable so that as soon as we change its value (by for eexample adding new elements) our view updates automagically.
@State var locationList = [
Location(id: 0, country: "Italy", state: "", town: "Finale Emilia"),
Location(id: 1, country: "Italy", state: "", town: "Bologna"),
Location(id: 2, country: "Italy", state: "", town: "Modena"),
Location(id: 3, country: "Italy", state: "", town: "Reggio Emilia"),
Location(id: 4, country: "USA", state: "CA", town: "Los Angeles")
]
// - Constants
private let locationManager = LocationManager()
// THIS IS NOT POSSIBLE WITH SWIFTUI AND GENERATES ERRORS
guard let exposedLocation = self.locationManager.exposedLocation else {
print("*** Error in \(#function): exposedLocation is nil")
return
}
var body: some View {
// Whenever we use a List based of an Array we have to let the List know how to identify each row as unique
// When confirming to the Identifiable protocol we no longer have to explicitly tell the List how the elements in our Array (which are conforming to that protocol) are uniquely identified
NavigationView {
// let’s add a title to our Navigation view and make sure you always do so on the first child view inside of your Navigation view
List(locationList) { location in
NavigationLink(destination: LocationDetail(location: location)) {
HStack {
Text(location.country)
Text(location.town).foregroundColor(.blue)
}
}
}
.navigationBarTitle(Text("Location"))
.navigationBarItems(
trailing: Button(action: addLocation, label: { Text("Add") }))
}
}
func addLocation() {
// We are using the native .randomElement() function of an Array to get a random element. The returned element however is optional. That is because in the case of the Array being empty that function would return nil. That’s why we append the returned value only in the case it doesn’t return nil.
if let randomLocation = locationList.randomElement() {
locationList.append(randomLocation)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You could create an ObservedObject
of your LocationManager
by implementing the ObservableObject
protocol.
With the @Published
attribute you can create a publisher object which notify the observers (your view, in this case) when something changes inside that object.
That's why in my LocationManager I added the @Published
attribute to those var:
locationStatus: CLAuthorizationStatus?
it contains the value received from didChangeAuthorization
delegate methodlastLocation: CLLocation?
it contains the last location calculated by the didUpdateLocations
delegate methodimport 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)
}
}
In your view you need to create only an instance of LocationManager
marked as @ObservedObject
import SwiftUI
struct MyView: View {
@ObservedObject var locationManager = LocationManager()
var userLatitude: String {
return "\(locationManager.lastLocation?.coordinate.latitude ?? 0)"
}
var userLongitude: String {
return "\(locationManager.lastLocation?.coordinate.longitude ?? 0)"
}
var body: some View {
VStack {
Text("location status: \(locationManager.statusString)")
HStack {
Text("latitude: \(userLatitude)")
Text("longitude: \(userLongitude)")
}
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}