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.