Region monitoring not working after 1 hour the app is killed.

This is my setup:

  • Granted always allow permission.
  • I have location added in UIBackgroundModes, but I did NOT set allowsBackgroundLocationUpdates to true

Note: I have this allowsBackgroundLocationUpdates = true set in my earlier version of app, which worked but we noticed it drained battery much faster, hence we removed all the settings that could affect battery.

The location model is setup with 20 regions, when boundary crossing happen, app sends a local notification. This works fine when app is in foreground/background. If app is killed, the app receives notification for boundary crossing only once.

Failed case for region monitoring:

  • Setup region monitoring
  • Kill the app
  • cross the boundary, app sends a local notification.
  • wait for 1 hour
  • leave the device in same state (notification is not opened, app is still killed state)
  • cross the boundary again
  • expect a notification, but app did not register any event related to region monitoring.

The console logs did not print anything in this second case.

public class LocationViewModel: NSObject, ObservableObject {
    private let maxMonitoredRegions = 20
    private var anyCancellable: AnyCancellable?
    private let locationManager: CLLocationManager
    @Published public var authorizationStatus: CLAuthorizationStatus
    @Published public var isMonitoringAvailable: Bool
    @Published public var monitoredRegions: [Region]
    @Published public var recentLocation: CLLocation?
    public var newlyEnteredRegionSignal = PassthroughSubject<CLRegion, Never>()
    public var recentLocationSignal = PassthroughSubject<CLLocation, Never>()
    public var authorizationStatusPublisher: Published<CLAuthorizationStatus>.Publisher { $authorizationStatus }
    
   public var isLocationEnabled: Bool {
      locationManager.authorizationStatus == .authorizedWhenInUse ||
      locationManager.authorizationStatus == .authorizedAlways
    }
    
    public override init() {
        locationManager = CLLocationManager()
        authorizationStatus = locationManager.authorizationStatus
        isMonitoringAvailable = CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self)

        monitoredRegions = []

        super.init()
        locationManager.delegate = self
        monitoredRegions.append(contentsOf: getMonitoredRegions())
        requestLocation()
    }

    public func requestLocation() {
        locationManager.requestLocation()
    }
    
    public func startRegionMonitoring(regions: [CLRegion]) {
        guard isMonitoringAvailable else {
            return
        }
        stopRegionMonitoring()
        if regions.isEmpty {
            return
        }
        if regions.count <= 20 {
          for region in regions {
            locationManager.startMonitoring(for: region)
          }
        } else {
          for region in regions[0...maxMonitoredRegions-1] {
            locationManager.startMonitoring(for: region)
          }
        }
    }

    public func stopRegionMonitoring() {
        guard isMonitoringAvailable else {
            return
        }

        if monitoredRegions.isEmpty {
            return
        }

        for region in monitoredRegions {
            let monitoredRegion = LocationUtils.convertRegionToCLRegion(region)
            locationManager.stopMonitoring(for: monitoredRegion)
        }
        monitoredRegions.removeAll()
    }

    private func getMonitoredRegions() -> [Region] {
        let monitoredRegions = locationManager.monitoredRegions

        var regions = [Region]()
        for monitoredRegion in monitoredRegions {
            if let region = LocationUtils.convertCLRegionToRegion(monitoredRegion) {
                regions.append(region)
            }
        }
        return regions
    }

    public func stopMonitoring() {
      recentLocation = nil
      stopRegionMonitoring()
    }
  
}

extension LocationViewModel: CLLocationManagerDelegate {
    public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        authorizationStatus = manager.authorizationStatus
        switch authorizationStatus {
        case .notDetermined:
          stopMonitoring()
        case .denied:
            stopMonitoring()
        case .authorizedAlways:
            break
        case .authorizedWhenInUse:
          // If user has requested whenInUse, request for always allow.
          locationManager.requestAlwaysAuthorization()
        @unknown default:
          break
        }

        if let location = manager.location {
          recentLocationSignal.send(location)
          recentLocation = location
        }
    }
  
    public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
      if let recentLocation = locations.last {
        self.recentLocation = recentLocation
        recentLocationSignal.send(recentLocation)
      }
    }
  
    public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        
    }

    public func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
        if let monitoredRegion = LocationUtils.convertCLRegionToRegion(region) {
            let oldRegion = monitoredRegions.first {
                $0.identifier == monitoredRegion.identifier
            }
            if oldRegion == nil {
                monitoredRegions.append(monitoredRegion)
            }
        }
    }

    public func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
       
    }

    public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        newlyEnteredRegionSignal.send(region)
    }

    public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        newlyEnteredRegionSignal.send(region)
    }
}

When app is awaken due to location event on app delegate, we initialize this location model, and location manager, and remove old monitored regions, and call startMonitoringRegions again, to keep the regions updated.

Please let me know if I'm missing any crucial information.

Too much code to analyze if something is wrong, but it sure looks like much ado for a simple thing. I'll leave the debugging of this to you.

But, you say for your test case:

  • Kill the app
  • cross the boundary, app sends a local notification.
  • wait for 1 hour
  • leave the device in same state (notification is not opened, app is still killed state)

The app is no longer necessarily in killed state once you crossed a boundary once as it was launched once already. If your subsequent region monitoring is relying on applicationDidFinishLaunching() thinking it will be called, it may or may not be called depending on what the system decides to do with your app within that hour.

That would be the first place I start looking.

Region monitoring not working after 1 hour the app is killed.
 
 
Q