Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get Current Location using SwiftUI, without ViewControllers?

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()
    }
}
like image 762
Salva Avatar asked Aug 27 '19 20:08

Salva


People also ask

How do I ask a user for location in Swift?

You need to add a key NSLocationWhenInUseUsageDescription in yours info. plist file and in the value you write something that you want to show the user in the popup dialog. You need to test it on a real device cause Simulator accepts only custom locations. Select the Simulator -> Debug -> Location -> Custom Location...

What is CLLocationManagerDelegate?

The object that you use to start and stop the delivery of location-related events to your app. Current page is CLLocationManagerDelegate. Apple.


1 Answers

You could create a StateObject 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:

  1. locationStatus: CLAuthorizationStatus? it contains the value received from didChangeAuthorization delegate method
  2. lastLocation: CLLocation? it contains the last location calculated by the didUpdateLocations delegate method

LocationManager

import Foundation
import CoreLocation
import Combine

class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {

    private let locationManager = CLLocationManager()
    @Published var locationStatus: CLAuthorizationStatus?
    @Published var lastLocation: CLLocation?

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

   
    
    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"
        }
    }

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

View

In your view you need to create only an instance of LocationManager marked as @StateObject

import SwiftUI

struct MyView: View {
    
    @StateObject 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()
    }
}

enter image description here

like image 89
Giuseppe Sapienza Avatar answered Nov 11 '22 10:11

Giuseppe Sapienza