简体   繁体   中英

How do I propertly subscribe to location updates when a View is visible?

I've created a class that implements CLLocationDelegate and ObservableObject so that it can let views subscribe to location updates using @StateObject and @EnvironmentObject . This works fine, but because only certain views need access to the location, I'd like to call startUpdatingLocation and stopUpdatingLocation when those views appear or disappear.

Assuming my view has access to the delegate and its location manager, where do I put the startUpdatingLocation and stopUpdatingLocation calls?

My first inclination was to use onAppear and onDisappear like so.

struct ViewThatNeedsLocation : View {
  @EnvironmentObject var locationDelegate: MyLocationDelegate

  var body: some View {
    Text("")
      .onAppear {
        locationDelegate.manager.startUpdatingLocation()
      }.onDisappear {
        locationDelegate.manager.stopUpdatingLocation()
      }
  }
}

But this suffers from a couple problems:

  • onAppear and onDisappear can be called multiple times due to parent changes, even if the user never sees the view visibly change.
  • onDisappear is not guaranteed to be called before a subsequent onAppear call.

This means I can get the following order of events

onAppear
  startUpdatingLocation (good)
onAppear (again)
  startUpdatingLocation (redundant)
onDisappear (?)
  stopUpdatingLocation (bad)

My guess is that ViewThatNeedsLocation is being created and destroyed multiple times without the rendered content being changed. This would be fine if SwiftUI provided some sort of guarantee about the order of the lifecycle modifiers, but it doesn't.

What is the correct way to do this? Is there a simple way to fire setup and teardown functions when a view appears and disappears reliably?

class LocationManager: NSObject, ObservableObject {

private let locationManager = CLLocationManager()
@Published var authorizationStatus: CLAuthorizationStatus = .notDetermined
@Published var location: CLLocation?
@Published var region: CLRegion?

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

extension LocationManager: CLLocationManagerDelegate {

func requestLocation() {
    locationManager.requestLocation()
}

func updateLocation() {
    locationManager.startUpdatingLocation()
}

func stopUpdatingLocation() {
    locationManager.stopUpdatingLocation()
}

func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
    self.authorizationStatus = status
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    if let location = locations.first {
        self.location = location
    }
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    print("error getting location \(error.localizedDescription)")
}
}

And in view struct:

 @ObservedObject var locationManager = LocationManager()
 @State private var location: CLLocation?

    .onReceive(locationManager.$location, perform: userLocation)
    .onReceive(locationManager.$authorizationStatus, perform: { status in
        switch status {
        case .authorizedAlways:
            locationManager.updateLocation()
        case .authorizedWhenInUse:
            locationManager.updateLocation()
        default:
            break
        }
    })

I just need first location so i am calling locationManager.stopUpdatingLocation() in my view in userLocation function, but you can do that in onDisapear. here you have some demo

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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