1

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?

Peter Sypek
  • 151
  • 2
  • 12
  • Share your code. Check are you enabled backgorund fetch access in capabilities. App background function only works 3 min, If you didn't mention in capabilities. – Mathi Arasan Aug 16 '17 at 06:33
  • Ok, i did not have the background fetch capabilities enabled. Only the background location service. I will try your suggestion. – Peter Sypek Aug 16 '17 at 06:55
  • The solution is still not working. Do i need to make the background work in a spezial background mode method? – Peter Sypek Aug 28 '17 at 06:10
  • Are you sure `StopMonitoringForOldRegions` this method call correctly?. Can you remove and check is your problem solved or not? [Check this Apple Document](https://developer.apple.com/documentation/corelocation/cllocationmanager) for alternative stop location monitor. – Mathi Arasan Aug 28 '17 at 07:18
  • Yes i'am sure. As long my app is in foreground it works as expected. Old monitored regions are removed and changed by new ones – Peter Sypek Aug 28 '17 at 12:59

0 Answers0