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 it along with significant location updates setting.
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)
}
}
AppDelegate:
if ([launchOptions objectForKey: UIApplicationLaunchOptionsLocationKey] != nil) {
[self hasLocationUpdate];
}
and
@objc public func hasLocationUpdate() {
locationModel = LocationViewModel()
locationModel.startRegionMonitoring(regions: [pass_regions_here])
}
When app is awaken due to location event on app delegate, I initialize this location model, and location manager, and remove old monitored regions, and call startMonitoringRegions again, to keep the regions updated.
Note: The test behaviour in simulator is not like this, simulator always worked. But what I observed in simulator is:
- For first time after app is killed, app is awaken by system when boundary crossing happened. I think it is a soft wake because the app doesn't exist in the background, when I checked app switcher the app is not listed there. Anyway, I got notification as expected here.
- For second time in simulator, the appdelegate
didFinishLaunchingdid not called, but my location model delegate methods got triggered, which tells , app still continues to be in the soft wake state from first time. The simulator did sent out a notification as expected, but the actual device is failing in this second case.
Now, if I open the app, and use it, I then get my notification delivered, which is supposed to be delivered in my second field test.
Please let me know if I'm missing any crucial information.