what is the best way to refresh geofence regions in background?
At the moment i use location service with background mode always.
I try to grab the userlocation
and compare the distance to an initial saved location. When the user moved a distance greater my half mapSpan
the app clears all monitoredRegions
, makes a firebase fetch
for search terms and performs a MKlocalSearchRequest
with the search terms.
All found MKMapItems
form the MKlocalSearchRequest
shoulb be set as new GeofenceRegion
.
It seems that the background refresh
of the monitoredRegions
are not working correctly.
When I start the app all initial nearby monitoredRegions
getting triggered. But when I drive a longer distance the background refresh
for new regions
seem not to update.
Do I need to use a different background operation to get it work as I want?
Here are some code details:
In the MKMapViewDelegate didUpdate userLocation
I look for my needs to update for new geofence regions.
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
lbl_DebugMonitoredRegions.text = "Current monitored regions: \(locationManager.monitoredRegions.count)"
self.userLocation = userLocation.coordinate
mapView.centerCoordinate = userLocation.coordinate
let region = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, CLLocationDistance(exactly: mapSpan)!, CLLocationDistance(exactly: mapSpan)!)
mapView.setRegion(region, animated: false)
if UserDefaults.standard.bool(forKey: eUserDefaultKey.isInitialLocationUpdate.rawValue){
//Update initial User Position
UpdateLastUserLocationFromUserDefaults(coordinate: userLocation.coordinate)
// Stop monitoring old regions
self.StopMonitoringForOldRegions()
//Remove old Geofence Overlays
self.RemoveOldGeofenceOverlays()
//Search nearby Shops
PerformLocalShopSearch()
}
else if UserDefaults.standard.bool(forKey: eUserDefaultKey.NeedToUpdateGeofence.rawValue){
// Stop monitoring old regions
self.StopMonitoringForOldRegions()
//Remove old Geofence Overlays
self.RemoveOldGeofenceOverlays()
//Search nearby Shops
PerformLocalShopSearch()
}
else if HasUserMovedDistanceGreaterMapSpan(userLocation: userLocation){
// Stop monitoring old regions
self.StopMonitoringForOldRegions()
//Remove old Geofence Overlays
self.RemoveOldGeofenceOverlays()
//Search nearby Shops
PerformLocalShopSearch()
}
}
Here I check for distance to saved initial position
//HasUserMovedDistanceGreaterMapSpan
private func HasUserMovedDistanceGreaterMapSpan(userLocation:MKUserLocation) -> Bool{
if let lastUserLocation = ReadLastUserLocationFromUserDefaults(){
let distance = userLocation.location?.distance(from: lastUserLocation)
if distance != nil && distance! > mapSpan * 0.25{
lbl_DebugHasUserMovedDistance.text = "User moved > mapSpan: \(debugcounter += 1)"
UpdateLastUserLocationFromUserDefaults(coordinate: userLocation.coordinate)
return true
}
}
return false
}
private func ReadLastUserLocationFromUserDefaults() -> CLLocation?{
let latitude = UserDefaults.standard.double(forKey: eUserDefaultKey.LastUserLatitude.rawValue)
let longitude = UserDefaults.standard.double(forKey: eUserDefaultKey.LastUserLongitude.rawValue)
if latitude > 0 && longitude > 0{
return CLLocation(latitude: CLLocationDegrees(floatLiteral: latitude), longitude: CLLocationDegrees(floatLiteral: longitude))
}
else { return nil }
}
private func UpdateLastUserLocationFromUserDefaults(coordinate: CLLocationCoordinate2D) -> Void{
//Set hasUserChangedGeofenceRadius false
UserDefaults.standard.set(false, forKey: eUserDefaultKey.NeedToUpdateGeofence.rawValue)
//Set isInitialLocationUpdate false
UserDefaults.standard.set(false, forKey: eUserDefaultKey.isInitialLocationUpdate.rawValue)
//SaveNew position
UserDefaults.standard.set(coordinate.latitude, forKey: eUserDefaultKey.LastUserLatitude.rawValue)
UserDefaults.standard.set(coordinate.longitude, forKey: eUserDefaultKey.LastUserLongitude.rawValue)
}
RemoveOldGeofenceOverlays and StopMonitoringForOldRegions
private func RemoveOldGeofenceOverlays() -> Void{
for overlay in self.MapView.overlays{
if overlay is MKUserLocation{ }
else { MapView.remove(overlay)}
}
}
func StopMonitoringForOldRegions(){
for region in locationManager.monitoredRegions {
locationManager.stopMonitoring(for: region)
NSLog("removing Region: " + region.identifier)
NSLog("Monitored regions \(self.locationManager.monitoredRegions.count)")
}
}
My PerformLocalShopSearch
private func PerformLocalShopSearch() -> Void{
let rad = UserDefaults.standard.double(forKey: eUserDefaultKey.MonitoredRadius.rawValue)
radiusToMonitore = CLLocationDistance(exactly: rad)
if radiusToMonitore == 0 { return }
firebaseShoppingList.GetStoresForGeofencing()
}
My delegate when stores are received from Firebase
func StoresCollectionReceived() {
for store in StoresArray {
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = store
request.region = MapView.region
let search = MKLocalSearch(request: request)
search.start { (response, error) in
if error != nil {
print(error!.localizedDescription)
return
}
if response!.mapItems.count == 0 {
NSLog("No local search matches found for \(store)")
return
}
NSLog("Matches found for \(store)")
self.StartMonitoringGeofenceRegions(mapItems: response!.mapItems)
}
}
}
And at least my functions for start monitoring circular regions
internal func StartMonitoringGeofenceRegions(mapItems: [MKMapItem]){
if self.userLocation == nil { return }
var possibleRegionsPerStore = Int(round(Double(StoresArray.count / 20)))
possibleRegionsPerStore = possibleRegionsPerStore < 4 ? 4: possibleRegionsPerStore
var itemsCount = 0
itemsCount = TryMonitoreRegion(mapItems: mapItems, possibleRegionsPerStore: possibleRegionsPerStore, itemsCount: itemsCount, minDistance: 0, maxDistance: mapSpan * 0.1)
if itemsCount == possibleRegionsPerStore { return }
itemsCount = TryMonitoreRegion(mapItems: mapItems, possibleRegionsPerStore: possibleRegionsPerStore, itemsCount: itemsCount, minDistance: mapSpan * 0.1, maxDistance: mapSpan * 0.2)
if itemsCount == possibleRegionsPerStore { return }
itemsCount = TryMonitoreRegion(mapItems: mapItems, possibleRegionsPerStore: possibleRegionsPerStore, itemsCount: itemsCount, minDistance: mapSpan * 0.2, maxDistance: mapSpan * 0.3)
if itemsCount == possibleRegionsPerStore { return }
itemsCount = TryMonitoreRegion(mapItems: mapItems, possibleRegionsPerStore: possibleRegionsPerStore, itemsCount: itemsCount, minDistance: mapSpan * 0.3, maxDistance: mapSpan * 0.4)
if itemsCount == possibleRegionsPerStore { return }
itemsCount = TryMonitoreRegion(mapItems: mapItems, possibleRegionsPerStore: possibleRegionsPerStore, itemsCount: itemsCount, minDistance: mapSpan * 0.4, maxDistance: mapSpan * 0.6)
if itemsCount == possibleRegionsPerStore { return }
itemsCount = TryMonitoreRegion(mapItems: mapItems, possibleRegionsPerStore: possibleRegionsPerStore, itemsCount: itemsCount, minDistance: mapSpan * 0.6, maxDistance: mapSpan * 3)
}
private func TryMonitoreRegion(mapItems:[MKMapItem], possibleRegionsPerStore:Int, itemsCount:Int, minDistance:Double, maxDistance:Double) -> Int{
for mapItem:MKMapItem in mapItems{
self.SetAnnotations(mapItem: mapItem)
if itemsCount == possibleRegionsPerStore { return itemsCount }
let distanceToUser = CalculateDistanceBetweenTwoCoordinates(location1: userLocation!, location2: mapItem.placemark.coordinate)
//Monitore 6th nearest stores if regions count still below 20
if locationManager.monitoredRegions.count < 20 && distanceToUser >= minDistance && distanceToUser <= maxDistance {
MonitoreCircularRegion(mapItem: mapItem)
return itemsCount + 1
}
}
return itemsCount
}
private func MonitoreCircularRegion(mapItem: MKMapItem){
DispatchQueue.main.async {
let region = CLCircularRegion(center: mapItem.placemark.coordinate, radius: CLLocationDistance(self.radiusToMonitore), identifier: "\(UUID().uuidString)\("SB_")\(mapItem.name!)")
self.locationManager.startMonitoring(for: region)
NSLog("Monitored Regions: \(self.locationManager.monitoredRegions.count)")
NSLog("MapSpan: \(self.mapSpan)")
NSLog("Start monitoring for Region: \(region) with Radius \(region.radius)" )
}
}
I still did not get updates in background when i move outside my mapspan. What I also observe is, that I get four times a response of two times loop in my MKLocalSearchRequest each request seem to deliver two responses. Is that correct?