Home/Code tips, Patches/Making the Location Sensor Work in the Background on iOS 9

Making the Location Sensor Work in the Background on iOS 9

Views:
247

A recent post in the Embarcadero Delphi forums asked about why the location sensor stopped working in the background when changing to iOS 9. Since I have an interest in this area, I’ve done a bit of investigating as to how to make this work, and this post is the result.

The location sensor stopped working because of a change in iOS 9 that requires a property to be set on CLLocationManager, in addition to having already include location in UIBackgroundModes in the project options. The property in question is allowsBackgroundLocationUpdates, however this property is not declared CLLocationManager in the iOSapi.CoreLocation unit, and so of course it is not surfaced as an option in TLocationSensor. A secondary issue relates to being able to set the “always” authorization instead of “when in use”.

In order to fix most issues such as this, I prefer to modify only the implementation sections of the units provided with Delphi. This is because I need only include those units when recompiling, rather than all of the units that are affected by a change if one is made in the interface section. This practice made this issue a challenge because I needed to change the interface provided in one unit (iOSapi.CoreLocation) that is used by another (System.iOS.Sensors). The answer was to completely redeclare CLLocationManager in System.iOS.Sensors in the implementation section, and add the new methods for the property there. Since the delegate is also declared in iOSapi.CoreLocation, I needed to redeclare that, too.

Here’s snippets (for brevity, and to avoid copyright issues) of the changes, which appear just after the uses clause in the implementation section of System.iOS.Sensors:

type
  // Reintroducing the entire CLLocationManager and CLLocationManagerDelegate in order to add allowsBackgroundLocationUpdates property
  CLLocationManager = interface(NSObject)
    ['{B162A514-4334-48B8-B31A-32926B758340}'] // New GUID
    function activityType : CLActivityType; cdecl;
    procedure allowDeferredLocationUpdatesUntilTraveled(distance: CLLocationDistance; timeout: NSTimeInterval); cdecl;
    function allowsBackgroundLocationUpdates: Boolean; cdecl; // New method for allowsBackgroundLocationUpdates property
    function delegate: Pointer; cdecl;
// snip
    procedure setActivityType(activityType: CLActivityType); cdecl;
    procedure setAllowsBackgroundLocationUpdates(allowsBackgroundLocationUpdates: Boolean); cdecl; // New method for allowsBackgroundLocationUpdates property
    procedure setDelegate(delegate: Pointer); cdecl;
// snip
  end;
  TCLLocationManager = class(TOCGenericImport<CLLocationManagerClass, CLLocationManager>)  end;

  CLLocationManagerDelegate = interface(IObjectiveC)
    ['{113A227F-AD2D-4983-83C3-C5158F394257}'] // New GUID
    procedure locationManager(manager: CLLocationManager; didFailWithError: NSError); overload; cdecl;
// snip
    [MethodName('locationManager:didFinishDeferredUpdatesWithError:')]
    procedure locationManagerDidFinishDeferredUpdatesWithError(manager: CLLocationManager; error: NSError); cdecl;
  end;

The next task was to work out a way of being able to optionally set the “always” authorization, and to optionally set the allowsBackgroundLocationUpdates property when the sensor is active. To achieve this, I modified TiOSLocationSensor.DoStart and used conditional defines so that the correct code is included at compile time:

function TiOSLocationSensor.DoStart: Boolean;
var
  I: Integer;
begin
  if TOSVersion.Check(8) and (FLocater <> nil) then
  begin
    // *** Added this conditional define
    {$IF Defined(REQUESTALWAYS)}
    FLocater.requestAlwaysAuthorization
    {$ELSE}
    FLocater.requestWhenInUseAuthorization;
    {$ENDIF}
  end;
  // *** Added this if block:
  if TOSVersion.Check(9) and (FLocater <> nil) then
  begin
    {$IF Defined(BACKGROUNDUPDATES) and Defined(CPUARM64)} // for some reason, this function crashes in 32-bit
    FLocater.setAllowsBackgroundLocationUpdates(True);
    {$ENDIF}
  end;
  // check authorization
  if Authorized = TAuthorizationType.atUnauthorized then
    SensorError(SLocationServiceUnauthorized);
// snip
end;

NOTE: Where the comment “//snip” appears, portions of the code have merely been left out in that section.

To test this all out, I’ve uploaded a test project:

 

You will still need to copy System.iOS.Sensors from your source folders, to somewhere in your project path, and make the changes described above. If you’re creating your own project, also ensure you have the conditional defines set, as well as location in the UIBackgroundModes option in the Version Information section of the project options.

By | 2016-09-15T13:34:38+00:00 February 27, 2016 12:44 pm|Code tips, Patches|27 Comments

About the Author:

27 Comments

  1. Antonello Carlomagno April 29, 2016 at 7:32 pm - Reply

    Hello,
    very interesting post.

    In this days I’m going crazy in order to listen the Beacon with application in background.

    Reading the documentation, I used UIBackgroundModes with Location and/or bluetooth-central mode …but nothing.

    I searched in internet but seems that this background mode not works.

    I need only that when I listen a beacon I open the app …

    Have you experience with this scenario ?

    thanks for suggestions.

    kind regards
    Antonello Carlomagno

  2. […] NOTE: This article is about allowing your iOS app to run when it goes into the background (i.e. another app becomes active) for a short period (up to 3 minutes on iOS 9, at least). It does not relate to having the UIBackgroundModes option, an example of which is here. […]

  3. Sean Hoffman August 13, 2016 at 12:50 am - Reply

    Hi have you filed a bug on this by chance?

    • admin August 14, 2016 at 3:12 pm - Reply

      Not as yet. I have a bit of a backlog; thanks for the reminder 🙂

  4. Elcio Signorini September 15, 2016 at 1:17 pm - Reply

    How are you, my name is Elcio , São Paulo, Brazil , tried to reproduce this script, bad this oroccrendo error in line with the script ” TCLLocationManager = class ( TOCGenericImport & lt; CLLocationManagerClass , CLLocationManager & gt; ) end ; ” I could not identify, can you help me another question , I’m trying to make my application running in the background on iOS 9.3.5 and can not in any way , I tried several examples found here bad without success, I will try to perform the functions using the location , really appreciate your help , and grateful greetings

    • admin September 15, 2016 at 1:37 pm - Reply

      Sorry; slight formatting error. It’s fixed now. As for the background problem, you’ll have to provide more information on what you want your code to do in the background

  5. Elcio Signorini September 15, 2016 at 2:07 pm - Reply

    Very grateful actually the application uses a timer and is from time to time sending via socket information of latitude and longitude , the current time and battery, already used the Feth and nothing would have any idea , grateful

    • admin September 15, 2016 at 3:13 pm - Reply

      You cannot use a timer in the background.

      The best option would be to set the LocationChange value on the location sensor to lctLarge when your app is about to go into the background, using the WillBecomeInactive event. You’ll also need to use the techniques described here:

      http://delphiworlds.com/2016/07/region-monitoring-background-ios/

      In order for the application to receive location updates in the background

  6. Elcio Signorini September 16, 2016 at 6:50 am - Reply

    Very grateful for helping me, I’m trying to play on my iOS changes in system.ios.sensors file and running lacationtest program, bad no success, maybe I’m putting the implementations in the wrong way, you could help me by sending the file system. ios.sensors with the proposed implementations, I thank you so much patience, hugs.

    • admin September 16, 2016 at 8:35 am - Reply

      I cannot send you the complete unit, as it would violate copyright.

      What do you mean by “no success”? Some things to check:

      1. You made a copy of the System.iOS.Sensors unit, and put it in the project path (in the same folder as the project itself should do)
      2. You made the changes to that unit outlined in the article
      3. You added the custom property of MFLocationRequestAlwaysPermission to the Version Info in the project options (as per the demo project) and set it to: true.
      4. Your application handles the WillBecomeInactive event, and sets the LocationChange value on the sensor to lctLarge. It should also handle WillBecomeForeground if you want the LocationChange set to lctSmall when the application is active. There is an example of how to handle these events, here:

      http://community.embarcadero.com/blogs/entry/mobile-app-lifecycle-events-handling-in-delphi-xe5-40067

      The final 2 steps are very important, as location changes in the background will be notified only if LocationChange is lctLarge, and only if the app has the “always” permission (meaning it has permission when in the foreground, and when in the background).

  7. Elcio Signorini September 16, 2016 at 12:51 pm - Reply

    Thankful , I understand the problem you encounter is only the function below manager.location emits error in the word location, this occurs when you insert the codes described , put the routines below the implamantation . Grateful

    TiOSLocationDelegate.locationManager procedure ( manager: CLLocationManager ; didUpdateToLocation , fromLocation : CLLocation ) ;
    begin
    DoStateChanged ( TSensorState.Ready ) ;
    try
    DoCLLocationChanged ( manager.location ) ;
    DoLocationChanged ( ConvCLLocation ( fromLocation )
    ConvCLLocation ( didUpdateToLocation ) ) ;
    finally
    DoStateChanged ( TSensorState.NoData ) ;
    end ;
    end ;

    • acarlomagno September 16, 2016 at 6:34 pm - Reply

      Hello,
      I do not know if it can help, with berlin I have changed only

      function TiOSLocationSensor.DoStart: Boolean;

      … adding just these two lines

      FLocater.requestAlwaysAuthorization
      FLocater.setAllowsBackgroundLocationUpdates(True);

      Now the app remains always in the background listening.

      Antonello

      • Elcio Signorini September 17, 2016 at 12:19 am - Reply

        Very grateful Antonello, worked perfectly, I will use the onlocationchange event to send information via socket, had any experience ?, the application worked perfectly in iOS8 after upgrade everything stopped, I have to redo the routines again, big hug

        Elcio

      • Elcio Signorini September 17, 2016 at 2:09 am - Reply

        Gentlemen, put the device to test and Locationstest application worked perfectly in the background, the problem that after 20 minutes it stops updating the positioning information, someone has been there.

  8. admin September 17, 2016 at 11:18 am - Reply

    If the app is in the background, and they do not change location significantly, there will be no updates after a while. Apparently, Apple don’t think that allowing your app to notify your server with an “I’m still here” message is OK.

    Regarding your query about iOS 8: yes, the location services changed in iOS 9 so that it needs that call

    setAllowsBackgroundLocationUpdates(True)

    In order to work in the background, even though the UIBackgroundModes already has location in it.

  9. acarlomagno September 17, 2016 at 5:40 pm - Reply

    ok, but If I want to create a app that inform me when I near a place..

    for example: I have 2 regions monitored, 1 for home and 1 for office.

    When I am near I receive a local push message…. it’s possible ?

    otherwise what serve the background mode if is not possible to use it ?

  10. Elcio Signorini September 18, 2016 at 3:36 am - Reply

    is there any way to wake up the place after hibernation?

    • admin September 18, 2016 at 8:03 pm - Reply

      If the app is still running in the background (using the code discussed, above), and the user changes location significantly, it will “wake up”. I don’t think there’s any way of making it “wake up” without the user changing location.

      • Elcio Signorini September 21, 2016 at 3:01 am - Reply

        Como vai, pode me ajudar, estou tentando escrever a primeira janela de sua orientação, más não sei em que lugar no arquivo system.ios.sensors devo colocar as funções, apenas alterei a função DoSstart incluindo os comandos, FLocater.requestAlwaysAuthorization;
        FLocater.setAllowsBackgroundLocationUpdates(True); o aparelho até funcionou em background, más depois de 1 hora ele para, em movimento, quando está de repouso, após 20 minutos ele também para e não volta após estar em movimento, pode me ajudar, muito grato

      • Elcio Signorini September 21, 2016 at 3:02 am - Reply

        How are you, help me, I’m trying to write the first window of his guidance, bad do not know where in the file system.ios.sensors I put the functions, just changed the DoSstart function including commands, FLocater.requestAlwaysAuthorization;
        FLocater.setAllowsBackgroundLocationUpdates (True); the device to work in the background, bad after 1 hour for it moving when resting, after 20 minutes he also not return after being on the go, you can help me, very grateful

        • admin September 21, 2016 at 9:33 am - Reply

          Can you show your code for DoStart?

          Also, remember that the LocationChange value for the TLocationSensor *must* be lctLarge when the application is in the background, otherwise it will not receive location updates

      • Elcio Signorini September 21, 2016 at 2:22 pm - Reply

        Hello, I’m rather putting lctLarge in background mode, below DoStart code, thanks.

        function TiOSLocationSensor.DoStart: Boolean;
        var
        I: Integer;
        begin
        // TOSVersion.Check if (8) and (FLocater nil) then
        //FLocater.requestWhenInUseAuthorization;
        FLocater.requestAlwaysAuthorization;
        FLocater.setAllowsBackgroundLocationUpdates (True);

        // Check authorization
        if Authorized = TAuthorizationType.atUnauthorized Then
        SensorError (SLocationServiceUnauthorized);
        // Check if location sensor is enabled
        Then if not FLocater.locationServicesEnabled
        SensorError (SLocationServiceDisabled);

        // Start location updates
        if (locationChange = TLocationChangeType.lctLarge) and CanUseSignifChangeNotifs Then
        FLocater.startMonitoringSignificantLocationChanges
        else
        FLocater.startUpdatingLocation;

        // Start heading updates
        Then if CanUseHeading
        begin
        FLocater.startUpdatingHeading;
        end;

        // Start monitoring regions
        Then if CanMonitorRegions
        for I: = 0 to Regions.Count – 1
        FLocater.startMonitoringForRegion (ConvLocationRegion (Regions [I]));
        Result: = FLocater.locationServicesEnabled;
        Then if Result
        Result: = = Authorized TAuthorizationType.atAuthorized;
        end;

      • Elcio Signorini September 21, 2016 at 2:26 pm - Reply

        Here again, the translator changed some lines

        function TiOSLocationSensor.DoStart: Boolean;
        var
        I: Integer;
        begin
        // TOSVersion.Check if (8) and (FLocater nil) then
        //FLocater.requestWhenInUseAuthorization;
        FLocater.requestAlwaysAuthorization;
        FLocater.setAllowsBackgroundLocationUpdates (True);

        // Check authorization
        if Authorized = TAuthorizationType.atUnauthorized Then
        SensorError (SLocationServiceUnauthorized);
        // Check if location sensor is enabled
        Then if not FLocater.locationServicesEnabled
        SensorError (SLocationServiceDisabled);

        // Start location updates
        if (locationChange = TLocationChangeType.lctLarge) and CanUseSignifChangeNotifs Then
        FLocater.startMonitoringSignificantLocationChanges
        else
        FLocater.startUpdatingLocation;

        // Start heading updates
        if CanUseHeading Then
        begin
        FLocater.startUpdatingHeading;
        end;

        // Start monitoring regions
        if CanMonitorRegions Then
        for I: = 0 to Regions.Count – 1
        FLocater.startMonitoringForRegion (ConvLocationRegion (Regions [I]));
        Result: = FLocater.locationServicesEnabled;
        Then if Result
        Result: = = Authorized TAuthorizationType.atAuthorized;
        end;

        • admin September 21, 2016 at 2:41 pm - Reply

          Aside from the obvious errors (probably caused by your translator), that looks OK. You might need to provide a small test project, so I could debug it and make sure everything is being set properly.

          I want to revisit the test project from my other article anyway, and trim it down to something very straightforward, but still achieve the result you’re looking for, however that may be some time from now.

          • Elcio Signorini September 21, 2016 at 2:52 pm

            Perfect , I’m waiting for the test project , any news please let me know, I’ve tried everything as far as my knowledge goes , the android had no problem , the problem appeared after upgrading from version 8 to version 9.3.5 IOS .
            Thank you

  11. Tristan Marlow November 14, 2016 at 3:07 pm - Reply

    Thanks for this sample works great. Just a little note, if you want to change from lctLarge and lctSmall during App background and foreground changes you need to stop and start the sensor.

Leave a Reply

Show Buttons
Hide Buttons