Get started with region monitoring (including in the background) on iOS with some how-to’s on fixing the Delphi RTL source for the LocationSensor.
In an earlier article, I described some changes you’d need to make in order to make monitoring of location changes work in the background with Delphi 10 Seattle. This article is aimed at Delphi 10.1 Berlin, where a couple of fixes have been made by Embarcadero since Delphi 10 Seattle, however with a little bit of work, should be able to be back ported to at least Seattle.
Not long ago I was asked by Marcelo Carvalho (a Delphi developer from Brazil) if I could look into fixing region monitoring, because he was having trouble adding regions to the LocationSensor once it was active, and also wanted to be able to monitor location changes for extended periods of time (> 12 hours). I asked Marcelo what the name of the project he was working on so I had something to refer to, and he said it wasn’t anything very specific yet, and since it is based on the “geofence” concept, he dubbed it “MonkeyFence”. He also wanted to pay forward the work to the Delphi community, so here it is.
The trouble starts in the System.iOS.Sensors unit: when a region is added to the sensor, the RegionAdded method is called. The region item passed in is “converted” to a CLRegion using the ConvLocationRegion method. In that method, a native CLRegion is created and initCircularRegionWithCenter is called.
Problem #1: The declaration for initCircularRegionWithCenter is wrong – the third parameter is declared in the Objective-C header as (NSString *), which is a pointer to an NSString, not an NSString itself.
Problem #2: initCircularRegionWithCenter has been deprecated since iOS 7, so unless you’re really targeting devices below that, then a CLCircularRegion should be used.
Problem #3: The initWithCenter method in CLCircularRegion is also declared incorrectly – I guess at least there’s consistency 😉
To start fixing these issues, copy System.iOS.Sensors.pas to somewhere in your project path and modify it, to redeclare CLCircularRegion with the correct method signature (you’ll note all my modifications are prefixed with a // MonkeyFence comment, and // snip appears where I have snipped the original source):
type // MonkeyFence - redeclared to correct the parameter list for initWithCenter CLCircularRegionClass = interface(CLRegionClass) ['{B2E71730-FB37-4DB4-9D49-8A004BB6C62C}'] end; CLCircularRegion = interface(CLRegion) ['{FF4DCF91-376B-41BB-B60A-880BEBB5B4EE}'] function initWithCenter(center: CLLocationCoordinate2D; radius: CLLocationDistance; identifier: Pointer): Pointer; cdecl; function center: CLLocationCoordinate2D; cdecl; function radius: CLLocationDistance; cdecl; function containsCoordinate(coordinate: CLLocationCoordinate2D): Boolean; cdecl; end; TCLCircularRegion = class(TOCGenericImport<CLCircularRegionClass, CLCircularRegion>) end;
Modify ConvLocationRegion to use TCLCircularRegion:
function ConvLocationRegion(const Region: TLocationRegion): CLRegion; var Center: CLLocationCoordinate2D; UniqueID: NSString; // MonkeyFence LCircularRegion: CLCircularRegion; begin Center := CLLocationCoordinate2DMake(Region.Center.Latitude, Region.Center.Longitude); // MonkeyFence UniqueID := StrToNSStr(Region.ID); // TNSString.Wrap(TNSString.OCClass.stringWithUTF8String(MarshaledAString(UTF8Encode(Region.ID)))); // create the region object and add it for monitorization // MonkeyFence - Deprecated method, and method signature is wrong anyway // Result := TCLRegion.Wrap(TCLRegion.Create.initCircularRegionWithCenter(Center, Region.Radius , UniqueID)); // MonkeyFence LCircularRegion := TCLCircularRegion.Create; LCircularRegion.initWithCenter(Center, Region.Radius, (UniqueID as ILocalObject).GetObjectID); Result := LCircularRegion; end;
Problem #4: TiOSLocationDelegate has a couple of pieces missing, namely the MethodName attributes for locationManagerDidEnterRegion and locationManagerDidExitRegion. Without them, those methods are never called – pretty important if the events are going to be fired:
// snip public { CLLocationManagerDelegate } procedure locationManager(manager: CLLocationManager; didChangeAuthorizationStatus: CLAuthorizationStatus); overload; cdecl; [MethodName('locationManager:didEnterRegion:')] // MonkeyFence procedure locationManagerDidEnterRegion(manager: CLLocationManager; region: CLRegion); cdecl; [MethodName('locationManager:didExitRegion:')] // MonkeyFence procedure locationManagerDidExitRegion(manager: CLLocationManager; region: CLRegion); cdecl; procedure locationManager(manager: CLLocationManager; didFailWithError: NSError); overload; cdecl; // snip
In my earlier article, I used a conditional define to signify if the app is to use the request always permission. Now through the magic of custom properties, the app will set the permission this way:
// MonkeyFence - put this before TiOSLocationSensor.DoStart function LocationRequestAlwaysPermission: Boolean; var LBundle: NSBundle; LPointer: Pointer; begin Result := False; LBundle := TNSBundle.Wrap(TNSBundle.OCClass.mainBundle); LPointer := LBundle.infoDictionary.valueForKey(StrToNSStr('MFLocationRequestAlwaysPermission')); // Do not localise if LPointer = nil then Exit; // <====== Result := NSStrToStr(TNSString.Wrap(LPointer)).Equals('true'); // Do not localise end;
Using a similar technique, the standard UIBackgroundModes property can be read so that setAllowsBackgroundUpdates can be called based on the property values:
// MonkeyFence - put this before TiOSLocationSensor.DoStart, too function HasBackgroundMode(const AMode: string): Boolean; var LBundle: NSBundle; LPointer: Pointer; LModesArray: NSArray; LModeString: string; I: Integer; begin Result := False; LBundle := TNSBundle.Wrap(TNSBundle.OCClass.mainBundle); LPointer := LBundle.infoDictionary.valueForKey(StrToNSStr('UIBackgroundModes')); // Do not localise if LPointer = nil then Exit; // <====== LModesArray := TNSArray.Wrap(LPointer); for I := 0 to LModesArray.count - 1 do begin LModeString := NSStrToStr(TNSString.Wrap(LModesArray.objectAtIndex(I))); if AMode.Equals(LModeString) then Exit(True); // <====== end; end;
Now TiOSLocationSensor.DoStart can be modified this way:
function TiOSLocationSensor.DoStart: Boolean; var I: Integer; begin // MonkeyFence if TOSVersion.Check(6) and (FLocater <> nil) then begin // Would be nice to have these as configurable within the application // https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/index.html#//apple_ref/occ/instp/CLLocationManager/pausesLocationUpdatesAutomatically //!!!! Can cause high battery consumption, but may be required for updates while in lock screen // FLocater.setPausesLocationUpdatesAutomatically(False); // https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/index.html#//apple_ref/occ/instp/CLLocationManager/activityType // Makes sure the location updates pause if the user is stationary for a while FLocater.setActivityType(CLActivityTypeFitness); end; if TOSVersion.Check(8) and (FLocater <> nil) then begin // MonkeyFence if LocationRequestAlwaysPermission then FLocater.requestAlwaysAuthorization else FLocater.requestWhenInUseAuthorization; end; // MonkeyFence if TOSVersion.Check(9) and (FLocater <> nil) and HasBackgroundMode('location') then begin {$IF Defined(CPUARM64)} // for some reason, this function crashes in 32bit FLocater.setAllowsBackgroundLocationUpdates(True); {$ENDIF} end; // snip
Problem #6: So now that locationManagerDidEnterRegion and locationManagerDidExitRegion are being called, yet another omission needs to be fixed, namely that the region identifier is not included in the TLocationRegion when it is initialised:
procedure TiOSLocationDelegate.locationManagerDidEnterRegion(manager: CLLocationManager; region: CLRegion); var LRegion: TLocationRegion; begin DoStateChanged(TSensorState.Ready); try LRegion := TLocationRegion.Create(ConvCLLocationCoord(region.center), region.radius, NSStrToStr(region.identifier)); // MonkeyFence // snip procedure TiOSLocationDelegate.locationManagerDidExitRegion(manager: CLLocationManager; region: CLRegion); var LRegion: TLocationRegion; begin DoStateChanged(TSensorState.Ready); try LRegion := TLocationRegion.Create(ConvCLLocationCoord(region.center), region.radius, NSStrToStr(region.identifier)); // MonkeyFence // snip
Phew! That’s all the changes needed for System.iOS.Sensors.
Problem #7: After I had worked out the fixes for this unit, Marcelo said he was also having trouble removing regions (using the RemoveRegion method of TLocationSensor), i.e. that it wouldn’t remove them at all. Long story short, another fix was required for the System.Sensors unit. Use the same process as for System.iOS.Sensors, i.e. make a copy and put it somewhere in your project path, then make the following changes – in the implementation uses clause:
uses System.Variants, System.Math, System.Character, System.Generics.Defaults, // MonkeyFence - System.Generics.Defaults added
..and in TCustomLocationSensor.Create:
constructor TCustomLocationSensor.Create(AManager: TSensorManager); begin inherited; // MonkeyFence - fixes the comparison for the Contains method of TRegionList FRegions := TRegionList.Create(TDelegatedComparer<TLocationRegion>.Create( function(const ALeft, ARight: TLocationRegion): Integer begin Result := 1; if (ALeft.ID = ARight.ID) and (ALeft.Radius = ARight.Radius) and (ALeft.Center.Latitude = ARight.Center.Latitude) and (ALeft.Center.Longitude = ARight.Center.Longitude) then Result := 0; end) ); FRegions.OnNotify := RegionNotify; end;
I count those changes as a workaround, because I was short on time and just needed to make removing regions work. If/when I have more time, I’ll revisit the actual cause, however it’s likely to do with the default comparer for record types.
Given that almost none of the code for region monitoring actually works, it’s an indicator of how much testing was done for it.
Now for the demo. Note that this demo really is a test project – it was pretty much thrown together in a couple of days or so, though you may note it leans towards using a MVVM type of arrangement, which is growing on me 🙂
A couple of things to note: it uses a UIBackgroundModes value of “location”, and a custom value of MFLocationRequestAlwaysPermission (as mentioned earlier), for all build configurations in iOS Device 32bit, iOSDevice 64bit and iOS Simulator.
It also includes a few random locations that are added to the sensor as regions when the app starts up, so if you fire it up in the simulator, then use Debug|Location (in the simulator) to change the location to somewhere inside the region, then outside the region, you’ll see the region enter/exit events in the app, in the Events window, and in the simulator log (Debug|Open System Log in the simulator)
As per other demos I have posted, rather than go too much into detail about the demo itself, I’ll leave it up to readers to ask questions here in the comments.
Hi Dave, I receive an error when compiling “[DCC Error] System.Sensors.pas(1097): E2034 Too many actual parameters”
pointing to TRegionList.Create(TDelegatedComparer.Create(
function(const ALeft, ARight: TLocationRegion) .
Hi Gordon,
Are you using Delphi 10.1 Berlin? The patches mentioned here are for that version. I need to revisit this article for Delphi 10.3 Rio, since some of the issues have been resolved.
Hi Dave, Sorry, yes I was using Rio.
Hello Dave, how are you?
can help me with background on Ios, I want to know how to keep the app active, even if closed. and the behavior is paused. I am from Brazil. thank you!
Which version of Delphi are you using? If you are using Delphi 10.4.2, please refer to this article:
https://delphiworlds.com/2020/01/cross-platform-location-monitoring/
Location updates should continue to occur even if the app is not running