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

Posted

技术标签:

【中文标题】iOS - 位置更改时 SwiftUI 更新文本【英文标题】:iOS - SwiftUI update text when location changes 【发布时间】:2021-03-08 09:59:26 【问题描述】:

我正在使用 SwiftUI 和 CLLocationManager。这是我的 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
    

现在我想显示当前坐标和城市,但只是没有显示城市,似乎变量没有正确更新。怎么做?这是我的看法:

@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)")
        
    

我不完全明白如何在位置更改或 getTown 函数完成关闭时将更新后的变量传递到视图中。

【问题讨论】:

如果您使用self.objectWillChange.send(),您希望视图仅在特定更改时刷新,而不是在每次更新时刷新。使用当前模型,您每次都在进行更新,因为属性也标记为@published,我认为这不是必需的,另一件事是您不需要显式定义此依赖关系var objectWillChange = ObservableObjectPublisher()。您可以在 getTown 函数中直接使用self.objectWillChange.send()。实际问题是经纬度更新? 这一切都很好,我可以在 getTown() (在位置管理器内部)中看到正在打印的城镇,但是当我尝试在文本标签的主视图中打印它时,它就出现了为空。你知道那里发生了什么吗? 您的getTown 函数将始终返回空字符串,因为您在该函数内部有async 调用,在点击CLGeocoder().reverseGeocodeLocation(location) 行后调用将直接返回调用,并返回空字符串。所以你的 userTown 总是“”。您可以直接在闭包块内使用userTown 变量,而不是Town 变量。 【参考方案1】:

你让事情变得比他们需要的更复杂。您不需要在模型中显式发布更改;属性标记为@Published,因此更改它们将自动触发属性更改。

您没有在视图中看到更新的原因是您尝试使用计算属性来访问您的模型;这行不通。没有任何东西可以消耗模型属性的已发布更改,也没有任何东西可以告诉视图它应该刷新。

如果您只是直接在 Text 视图中访问视图模型属性,它将按照您想要的方式工作。

您的最终问题与反向地理编码有关。首先,反向地理编码请求异步完成。这意味着您不能return town。同样,您可以简单地直接更新 userTown 属性,将其分派到主队列,因为您不能保证将在主队列上调用反向地理编码处理程序,并且必须在主队列上执行所有 UI 更新。

将所有这些放在一起得到

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

反向地理编码的最后一个问题是速率受限;在开始出现错误之前,您只能在一段时间内调用它多次。位置更新大约每秒到达一次,即使您没有移动也是如此。大多数情况下,您会不必要地查找相同或几乎相同的位置。

一种方法是检查自上次反向位置查找以来行进的距离,仅在超过某个阈值(例如 500 米)时才执行新查找

(我们也可以使用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
            
        
        
    

【讨论】:

以上是关于iOS - 位置更改时 SwiftUI 更新文本的主要内容,如果未能解决你的问题,请参考以下文章

@ObservedObject 更改后 SwiftUI 视图未更新

根据 SwiftUI 中的切换值更改文本

状态更改时自定义 SwiftUI 视图不会更新

UITextField 未更新以更改 ObservableObject (SwiftUI)

SwiftUI:具有计时器样式的文本在提供的日期更改后不会更新

SwiftUI Live 位置更新