This is my setup:
- Granted always allow permission.
- I have location added in
UIBackgroundModes
, but I did NOT setallowsBackgroundLocationUpdates
totrue
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.