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):

[sourcecode language=”delphi”]

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;

[/sourcecode]

Modify ConvLocationRegion to use TCLCircularRegion:

[sourcecode language=”delphi”]

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;

[/sourcecode]

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:

[sourcecode language=”delphi”]

// 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

[/sourcecode]

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:

[sourcecode language=”delphi”]

// 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;

[/sourcecode]

Using a similar technique, the standard UIBackgroundModes property can be read so that setAllowsBackgroundUpdates can be called based on the property values:

[sourcecode language=”delphi”]

// 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;

[/sourcecode]

Now TiOSLocationSensor.DoStart can be modified this way:

[sourcecode language=”delphi”]

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

[/sourcecode]

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:

[sourcecode language=”delphi”]

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

[/sourcecode]

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:

[sourcecode language=”delphi”]

uses
System.Variants, System.Math, System.Character, System.Generics.Defaults, // MonkeyFence – System.Generics.Defaults added

[/sourcecode]

..and in TCustomLocationSensor.Create:

[sourcecode language=”delphi”]

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;

[/sourcecode]

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.