简体   繁体   English

iOS - SwiftUI 在位置更改时更新文本

[英]iOS - SwiftUI update text when location changes

I am working with SwiftUI and a CLLocationManager.我正在使用 SwiftUI 和 CLLocationManager。 Here is my LocationModel:这是我的位置模型:

class LocationViewModel: NSObject, ObservableObject{
  
  @Published var userLatitude: Double = 0
  @Published var userLongitude: Double = 0
  @Published var userTown: String = ""
    var objectWillChange = ObservableObjectPublisher()
  
  private let locationManager = CLLocationManager()
  
  override init() {
    super.init()
    self.locationManager.delegate = self
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
    self.locationManager.requestWhenInUseAuthorization()
    self.locationManager.startUpdatingLocation()
  }
}

extension LocationViewModel: CLLocationManagerDelegate {
    
    struct ReversedGeoLocation {
        let name: String            // eg. Apple Inc.
        let streetName: String      // eg. Infinite Loop
        let streetNumber: String    // eg. 1
        let city: String            // eg. Cupertino
        let state: String           // eg. CA
        let zipCode: String         // eg. 95014
        let country: String         // eg. United States
        let isoCountryCode: String  // eg. US

        var formattedAddress: String {
            return """
            \(name),
            \(streetNumber) \(streetName),
            \(city), \(state) \(zipCode)
            \(country)
            """
        }

        // Handle optionals as needed
        init(with placemark: CLPlacemark) {
            self.name           = placemark.name ?? ""
            self.streetName     = placemark.thoroughfare ?? ""
            self.streetNumber   = placemark.subThoroughfare ?? ""
            self.city           = placemark.locality ?? ""
            self.state          = placemark.administrativeArea ?? ""
            self.zipCode        = placemark.postalCode ?? ""
            self.country        = placemark.country ?? ""
            self.isoCountryCode = placemark.isoCountryCode ?? ""
        }
    }
  
  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.last else { return }
    userLatitude = location.coordinate.latitude
    userLongitude = location.coordinate.longitude
    userTown = getTown(lat: CLLocationDegrees.init(userLatitude), long: CLLocationDegrees.init(userLongitude))

    print(location)
  }
    
    func getTown(lat: CLLocationDegrees, long: CLLocationDegrees) -> String
    {
        var town = ""
        let location = CLLocation.init(latitude: lat, longitude: long)
        CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in

            guard let placemark = placemarks?.first else {
                let errorString = error?.localizedDescription ?? "Unexpected Error"
                print("Unable to reverse geocode the given location. Error: \(errorString)")
                return
            }

            let reversedGeoLocation = ReversedGeoLocation(with: placemark)
            print(reversedGeoLocation.city)
            
            town = reversedGeoLocation.city
            self.objectWillChange.send()
        }
        
        return town
    }
}

Now I would like to display the current coordinates and the city, but the city is just not being displayed, seems that the variable is not being updated properly.现在我想显示当前坐标和城市,但只是没有显示城市,似乎变量没有正确更新。 How to do that?怎么做? Here is my view:这是我的看法:

@ObservedObject var locationViewModel = LocationViewModel()

    var latitude: Double  { return(locationViewModel.userLatitude ) }
    var longitude: Double { return(locationViewModel.userLongitude ) }
    var town: String { return(locationViewModel.userTown) }

    var body: some View {
        VStack {
            Text("Town: \(town)")
            Text("Latitude: \(latitude)")
            Text("Longitude: \(longitude)")
        }
    }

I do not completely understand how to pass the updated variable into the view when the location changes or when the completion of the getTown functions closes.我不完全了解如何在位置更改或 getTown 函数完成时将更新的变量传递到视图中。

You have made things more complicated than they need to be.你让事情变得比他们需要的更复杂。 You don't need to explicitly publish changes in your model;您无需在 model 中显式发布更改; The properties are marked as @Published , so changing them will automatically fire the property change.属性标记为@Published ,因此更改它们将自动触发属性更改。

The reason you aren't seeing updates in your view is because you have tried to use computed properties to access your model;您没有在视图中看到更新的原因是您尝试使用计算属性来访问您的 model; This won't work.这行不通。 There is nothing to consume the published changes to your model properties and nothing to tell the view that it should refresh.没有任何东西可以消耗你的 model 属性的已发布更改,也没有任何东西可以告诉视图它应该刷新。

If you simply access the view model properties directly in your Text views it will work the way you want.如果您只是直接在您的Text视图中访问视图 model 属性,它将按照您想要的方式工作。

Your final problems are related to reverse geocoding.您的最终问题与反向地理编码有关。 First, a reverse geocoding request completes asynchronously.首先,反向地理编码请求异步完成。 This means you can't return town .这意味着你不能return town Again, you can simply update the userTown property directly, dispatching onto the main queue since you can't guarantee that the reverse geocoding handler will be called on the main queue and all UI updates must be performed on the main queue.同样,您可以简单地直接更新userTown属性,将其分派到主队列,因为您不能保证将在主队列上调用反向地理编码处理程序,并且必须在主队列上执行所有 UI 更新。

Putting all of this together gets把所有这些放在一起得到

class LocationViewModel: NSObject, ObservableObject{
  
  @Published var userLatitude: Double = 0
  @Published var userLongitude: Double = 0
  @Published var userTown: String = ""
  
  private let locationManager = CLLocationManager()
  
  override init() {
    super.init()
    self.locationManager.delegate = self
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
    self.locationManager.requestWhenInUseAuthorization()
    self.locationManager.startUpdatingLocation()
  }
}

extension LocationViewModel: CLLocationManagerDelegate {
    
    struct ReversedGeoLocation {
        let name: String            // eg. Apple Inc.
        let streetName: String      // eg. Infinite Loop
        let streetNumber: String    // eg. 1
        let city: String            // eg. Cupertino
        let state: String           // eg. CA
        let zipCode: String         // eg. 95014
        let country: String         // eg. United States
        let isoCountryCode: String  // eg. US

        var formattedAddress: String {
            return """
            \(name),
            \(streetNumber) \(streetName),
            \(city), \(state) \(zipCode)
            \(country)
            """
        }

        // Handle optionals as needed
        init(with placemark: CLPlacemark) {
            self.name           = placemark.name ?? ""
            self.streetName     = placemark.thoroughfare ?? ""
            self.streetNumber   = placemark.subThoroughfare ?? ""
            self.city           = placemark.locality ?? ""
            self.state          = placemark.administrativeArea ?? ""
            self.zipCode        = placemark.postalCode ?? ""
            self.country        = placemark.country ?? ""
            self.isoCountryCode = placemark.isoCountryCode ?? ""
        }
    }
  
  func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.last else { return }
    userLatitude = location.coordinate.latitude
    userLongitude = location.coordinate.longitude
    getTown(lat: CLLocationDegrees.init(userLatitude), long: CLLocationDegrees.init(userLongitude))

    print(location)
  }
    
    func getTown(lat: CLLocationDegrees, long: CLLocationDegrees) -> Void
    {
        let location = CLLocation.init(latitude: lat, longitude: long)
        CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in

            guard let placemark = placemarks?.first else {
                let errorString = error?.localizedDescription ?? "Unexpected Error"
                print("Unable to reverse geocode the given location. Error: \(errorString)")
                return
            }

            let reversedGeoLocation = ReversedGeoLocation(with: placemark)
            print(reversedGeoLocation.city)
            DispatchQueue.main.async {
                self.userTown = reversedGeoLocation.city
            }
        }

    }
}

struct ContentView: View {
    @ObservedObject var locationViewModel = LocationViewModel()
    
    var body: some View {
        VStack {
            Text("Town: \(locationViewModel.userTown)")
            Text("Latitude: \(locationViewModel.userLatitude)")
            Text("Longitude: \(locationViewModel.userLongitude)")
        }
    }
}

The final problem with reverse geocoding is that it is rate limited;反向地理编码的最后一个问题是它受到速率限制。 you can only call it so many times in a period before you will start getting errors.在开始出现错误之前,您只能在一段时间内调用它多次。 Location updates arrive about once per second, even when you aren't moving.位置更新大约每秒到达一次,即使您没有移动也是如此。 Most of the time you will be needlessly looking up the same or almost the same location.大多数情况下,您将不必要地查找相同或几乎相同的位置。

One approach is to check the distance travelled since the last reverse location look up and only perform a new look up if you exceed some threshold, say 500m一种方法是检查自上次反向位置查找以来经过的距离,并且仅在超过某个阈值(例如 500m)时才执行新查找

(We can also be a bit smarter with getTown - There is no point in splitting a location into latitude/longitude only to create a CLLocation in getTown ) (我们也可以使用getTown更聪明一些 - 将位置拆分为纬度/经度只是为了在getTown中创建一个CLLocation是没有意义的)


private var lastTownLocation: CLLocation? = nil

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        userLatitude = location.coordinate.latitude
        userLongitude = location.coordinate.longitude
        
        if self.lastTownLocation == nil || self.lastTownLocation!.distance(from: location) > 500 {
            getTown(location)
        }
    }
    
    func getTown(_ location: CLLocation) -> Void
    {
        CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
            
            guard let placemark = placemarks?.first else {
                let errorString = error?.localizedDescription ?? "Unexpected Error"
                print("Unable to reverse geocode the given location. Error: \(errorString)")
                return
            }
            self.lastTownLocation = location
            let reversedGeoLocation = ReversedGeoLocation(with: placemark)
            print(reversedGeoLocation.city)
            DispatchQueue.main.async {
                self.userTown = reversedGeoLocation.city
            }
        }
        
    }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM