更新的反向地理定位

Updated Reverse Geolocation

有许多示例展示了如何进行反向地理定位,但最近没有关于在 SwiftUI 中实现的内容。我当前的代码使用 iPhone GPS 生成坐标,这些坐标与地图一起使用以显示位置。我还想显示街道地址,因为没有文本指示位置的地图不是很有用。

我的问题:

  1. 我是否拥有实现反向地理定位的所有相关代码?
  2. 我看过使用情节提要和打印语句显示位置的示例,但是我如何 return 使用 @escaping 闭包将位置显示到 Swiftui 视图?
import Foundation
import CoreLocation


class LocationManager: NSObject, ObservableObject {
   
   private let locationManager = CLLocationManager()
   
   @Published var currentAddress: String = ""
   
   override init() {
       super.init()
       
       self.locationManager.delegate = self
       self.locationManager.distanceFilter = 10 // distance before update (meters)
       self.locationManager.requestWhenInUseAuthorization()
       self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
       self.locationManager.startUpdatingLocation()
   }
   
   func startLocationServices() {
       
       if locationManager.authorizationStatus == .authorizedAlways || locationManager.authorizationStatus == .authorizedWhenInUse {
           locationManager.startUpdatingLocation()
       } else {
           
           locationManager.requestWhenInUseAuthorization()
       }
   }
   
   func getLocationCoordinates() -> (Double, Double) {
       
       let coordinate = self.locationManager.location != nil ? self.locationManager.location!.coordinate : CLLocationCoordinate2D()
       print("location = \(coordinate.latitude), \(coordinate.longitude)")
       
       return (Double(coordinate.latitude), Double(coordinate.longitude))
   }
   
   // Using closure
   func getAddress(handler: @escaping (String) -> Void)
   {
       self.currentAddress = ""
       
       let coordinate = self.locationManager.location != nil ? self.locationManager.location!.coordinate : CLLocationCoordinate2D()
       
       let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
       
       let geoCoder = CLGeocoder()
       geoCoder.reverseGeocodeLocation(location, completionHandler: { (placemarks, error) -> Void in
           
           // Place details
           var placeMark: CLPlacemark?
           placeMark = placemarks?[0]
           
           guard let placemark = placemarks?.first else { return }
           if let streetNumber = placemark.subThoroughfare,
              let street = placemark.subThoroughfare,
              let city = placemark.locality,
              let state = placemark.administrativeArea {
               DispatchQueue.main.async {
                   self.currentAddress = "\(streetNumber) \(street) \(city) \(state)"
               }
           } else if let city = placemark.locality, let state = placemark.administrativeArea {
               DispatchQueue.main.async {
                   self.currentAddress = "\(city) \(state)"
               }
           } else {
               DispatchQueue.main.async {
                   self.currentAddress = "Address Unknown"
               }
           }
       }
       )
       print( self.currentAddress)
   }
}
extension LocationManager: CLLocationManagerDelegate {
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        if locationManager.authorizationStatus == .authorizedAlways || locationManager.authorizationStatus == .authorizedWhenInUse {
            locationManager.startUpdatingLocation()
        }
    }
    
    // Get Placemark
    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)
        }
    }
}

如果我在 ContentView 中添加以下代码:

    @State private var entryLat: Double = 0.0
    @State private var entryLong: Double = 0.0

    let result = lm.getLocationCoordinates()
    entryLat = result.0
    entryLong = result.1

如何调用 getPlace?

要使用以下代码,您需要设置适当的权利和权限。 这是一个在 swiftui 中使用地理定位的工作示例,来自我从 几年前网络上的一些来源。 这应该为您提供在 swiftui 中进行反向地理定位的基础:

import Foundation
import CoreLocation
import SwiftUI
import Combine


@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    let locationProvider = LocationProvider()
    @State var currentAddress = ""
    
    var body: some View {
        Text(currentAddress)
            .onAppear {
                getAddress()
            }
    }
    
    func getAddress() {
        // for testing  Tokyo
        let location = CLLocation(latitude: 35.684602, longitude: 139.751992)
        
        locationProvider.getPlace(for: location) { plsmark in
            guard let placemark = plsmark else { return }
            if let streetNumber = placemark.subThoroughfare,
               let street = placemark.subThoroughfare,
               let city = placemark.locality,
               let state = placemark.administrativeArea {
                self.currentAddress = "\(streetNumber) \(street) \(city) \(state)"
            } else if let city = placemark.locality, let state = placemark.administrativeArea {
                self.currentAddress = "\(city) \(state)"
            } else {
                self.currentAddress = "Address Unknown"
            }
        }
    }
    
}

/**
 A Combine-based CoreLocation provider.
 
 On every update of the device location from a wrapped `CLLocationManager`,
 it provides the latest location as a published `CLLocation` object and
 via a `PassthroughSubject<CLLocation, Never>` called `locationWillChange`.
 */
public class LocationProvider: NSObject, ObservableObject {
    
    private let lm = CLLocationManager()
    
    /// Is emitted when the `location` property changes.
    public let locationWillChange = PassthroughSubject<CLLocation, Never>()
    
    /**
     The latest location provided by the `CLLocationManager`.
     
     Updates of its value trigger both the `objectWillChange` and the `locationWillChange` PassthroughSubjects.
     */
    @Published public private(set) var location: CLLocation? {
        willSet {
            locationWillChange.send(newValue ?? CLLocation())
        }
    }
    
    /// The authorization status for CoreLocation.
    @Published public var authorizationStatus: CLAuthorizationStatus?
    
    /// A function that is executed when the `CLAuthorizationStatus` changes to `Denied`.
    public var onAuthorizationStatusDenied : ()->Void = {presentLocationSettingsAlert()}
    
    /// The LocationProvider intializer.
    ///
    /// Creates a CLLocationManager delegate and sets the CLLocationManager properties.
    public override init() {
        super.init()
        self.lm.delegate = self
        self.lm.desiredAccuracy = kCLLocationAccuracyBest
        self.lm.activityType = .fitness
        self.lm.distanceFilter = 10
        self.lm.allowsBackgroundLocationUpdates = true
        self.lm.pausesLocationUpdatesAutomatically = false
        self.lm.showsBackgroundLocationIndicator = true
    }
    
    /**
     Request location access from user.
     
     In case, the access has already been denied, execute the `onAuthorizationDenied` closure.
     The default behavior is to present an alert that suggests going to the settings page.
     */
    public func requestAuthorization() -> Void {
        if self.authorizationStatus == CLAuthorizationStatus.denied {
            onAuthorizationStatusDenied()
        }
        else {
            self.lm.requestWhenInUseAuthorization()
        }
    }
    
    /// Start the Location Provider.
    public func start() throws -> Void {
        self.requestAuthorization()
        
        if let status = self.authorizationStatus {
            guard status == .authorizedWhenInUse || status == .authorizedAlways else {
                throw LocationProviderError.noAuthorization
            }
        }
        else {
            /// no authorization set by delegate yet
#if DEBUG
            print(#function, "No location authorization status set by delegate yet. Try to start updates anyhow.")
#endif
            /// In principle, this should throw an error.
            /// However, this would prevent start() from running directly after the LocationProvider is initialized.
            /// This is because the delegate method `didChangeAuthorization`,
            /// setting `authorizationStatus` runs only after a brief delay after initialization.
            //throw LocationProviderError.noAuthorization
        }
        self.lm.startUpdatingLocation()
    }
    
    /// Stop the Location Provider.
    public func stop() -> Void {
        self.lm.stopUpdatingLocation()
    }
    
    // todo deal with errors
    public func getPlace(for location: CLLocation, completion: @escaping (CLPlacemark?) -> Void) {
        let geocoder = CLGeocoder()
        geocoder.reverseGeocodeLocation(location) { placemarks, error in
            guard error == nil else {
                print("=====> Error \(error!.localizedDescription)")
                completion(nil)
                return
            }
            guard let placemark = placemarks?.first else {
                print("=====> Error placemark is nil")
                completion(nil)
                return
            }
            completion(placemark)
        }
    }
    
}

/// Present an alert that suggests to go to the app settings screen.
public func presentLocationSettingsAlert(alertText : String? = nil) -> Void {
    let alertController = UIAlertController (title: "Enable Location Access", message: alertText ?? "The location access for this app is set to 'never'. Enable location access in the application settings. Go to Settings now?", preferredStyle: .alert)
    let settingsAction = UIAlertAction(title: "Settings", style: .default) { (_) -> Void in
        guard let settingsUrl = URL(string:UIApplication.openSettingsURLString) else {
            return
        }
        UIApplication.shared.open(settingsUrl)
    }
    alertController.addAction(settingsAction)
    let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
    alertController.addAction(cancelAction)
    UIApplication.shared.windows[0].rootViewController?.present(alertController, animated: true, completion: nil)
}


/// Error which is thrown for lacking localization authorization.
public enum LocationProviderError: Error {
    case noAuthorization
}

extension LocationProvider: CLLocationManagerDelegate {
    
    public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        self.authorizationStatus = status
#if DEBUG
        print(#function, status.name)
#endif
        //print()
    }
    
    public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        self.location = location
    }
    
    public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        if let clErr = error as? CLError {
            switch clErr {
            case CLError.denied : do {
                print(#function, "Location access denied by user.")
                self.stop()
                self.requestAuthorization()
            }
            case CLError.locationUnknown : print(#function, "Location manager is unable to retrieve a location.")
            default: print(#function, "Location manager failed with unknown CoreLocation error.")
            }
        }
        else {
            print(#function, "Location manager failed with unknown error", error.localizedDescription)
        }
    }
}

extension CLAuthorizationStatus {
    /// String representation of the CLAuthorizationStatus
    var name: String {
        switch self {
        case .notDetermined: return "notDetermined"
        case .authorizedWhenInUse: return "authorizedWhenInUse"
        case .authorizedAlways: return "authorizedAlways"
        case .restricted: return "restricted"
        case .denied: return "denied"
        default: return "unknown"
        }
    }
}