Geolocation with Titanium — Part 2

Geolocation

In Part 1 of this series, I showed how you can get the device location and track location changes when the app is in the foreground. That’s great but what happens when the app is in the background?

Geolocation with Titanium

Background geolocation can be complex on one platform, but when you’re dealing with multiple platforms and a cross-platform API that has to provide a consistent method for different devices, then it gets even more challenging.

So in this post, I’ll be describing how to implement background location updates for iOS (and a follow-up post will cover Android).

To give this some context, I’ll be interspersing this post with references to an app I worked on recently where background location was a key feature of the app. The app was much like the “Find My” app in iOS, specifically focused on locating people within specific distances of you. The app needed to be able to track location updates in the background efficiently and be able to locate a nearby user running iOS or Android as close as 25 meters away.

The first thing we need to do is add some permissions to the app so we can use location services in the background, so in the TiApp.xml file we need to add the background location permission. We do this by adding the background activity to the iOS section — here’s a simplified version of mine:

<ios>
        <min-ios-ver>10.0</min-ios-ver>
        <plist>
            <dict>                
                <key>NSLocationWhenInUseUsageDescription</key>
                <string>We need your location to find other users</string>
                <key>NSLocationWhenInUseUsageDescription</key>
                <string>We need your location to find other users</string>
                <key>NSLocationAlwaysUsageDescription</key>
                <string>We need your location to find other users</string>
                <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
                <string>We need your location to find other users</string>
                <key>UIBackgroundModes</key>
                <array>
                    <string>location</string>
                    <string>remote-notification</string>
                </array>
            </dict>
        </plist>
    </ios>

You’ll notice I also have remote-notification as permission too — we’ll come back to why that’s there later.

A key point here is that the when in use permissions aren’t any good if you want to capture location in the background but you should add code to your app to check for this each time it’s launched or resumed just in case the user has changed the permission in system settings. In these cases, you can explain to the user that they have when in use selected and what this means. Ultimately, the choice is theirs whether or not they allow that.

Another important thing to mention is that with the recent release of iOS 13, Apple has changed the way that they show users permission alerts. On iOS 12, users would show a dialog when your app asks for permission offering the different methods you’ve requested, so While in Use, Always on etc. With iOS 13, users won’t see the Always option — they’ll only see When in use and One time only.

If your app is backgrounded and requests a background location update/permission then the user will see a permission dialog pop up—it’s at that point they can allow always access. They’ll also be reminded from time-to-time that your app is in the background and using location services and be asked if they want to change permissions — so it’s really important to take note of that caveat and ensure you’re checking for permissions whenever you try to get location updates.

With permissions in place, we can now add code to get location updates in the background. There are a few ways to do this. iOS has a selection of modes for geolocation updates, from walking/activity to running and turn by turn navigation. Each one has varying degrees of initial accuracy and power usage, with navigation modes using the most.

Let’s get started. First, I’ve created a new file in app/lib/ called gps.js and will use this to handle the background location updates.

I’m simplifying a lot of the code examples here but a the end of the post you’ll find a link to a repo with a more complete app example

The first thing I’m going to do in my app is set up Ti.Geolocation with the parameters I want to use (for IOS). I’m dropping the following into the top of that file:

Ti.Geolocation.showBackgroundLocationIndicator = false;
Ti.Geolocation.allowsBackgroundLocationUpdates = true;
Ti.Geolocation.pauseLocationUpdateAutomatically = false;
Ti.Geolocation.trackSignificantLocationChange = false;
Ti.Geolocation.accuracy = Ti.Geolocation.ACCURACY_BEST_FOR_NAVIGATION;

With this code, I’m setting a flag to hide the background location indicator (the blue status indicator that tells you an app is using location services in the background). You can choose to show this is you like (it’s useful for debugging).

Next, I’m setting Geolocation to use background updates. If you don’t do this then background location won’t work even if you’ve configured permissions etc in the Tiapp.xml file.

For accuracy I’m going with ACCURACY_BEST_FOR_NAVIGATION which gives the highest possible accuracy by combining GPS and other sensor data. You could change this to use any of the other accuracy levels.

Once I’ve setup location properties I need to some basic functions in place. First, I’ll drop in the following function which I can use later to check for location permissions:

function getLocationPermissions(callback) {
    Ti.Geolocation.requestLocationPermissions(Ti.Geolocation.AUTHORIZATION_ALWAYS, function (result) {
        if (result.success) {
            console.log(“gps — location permissions granted”);
            if (callback) callback();
        } else {
            Ti.Geolocation.requestLocationPermissions(Ti.Geolocation.AUTHORIZATION_WHEN_IN_USE, function (result) {
                if (result.success) {
                    alert(“We will only be able to track you when the app is running. You can change this in your device location settings.”);
                    if (callback) callback();
                } else {
                    alert(“Please enable location services for this app in your device settings.”);
                }
            });
        }
    });
}

Next, let’s add a function to get the current location:

exports.getCurrentGeoLocation = function (callback) {
    getLocationPermissions(function () {
        Ti.Geolocation.getCurrentPosition(function (pos) {
            locationUpdateHandler(pos, callback);
        });
    });
};

You’ll notice that this function is calling out getLocationPermissions function so it’s always checking if the user has changed something in the device settings.

It’s also calling a locationUpdateHandler so let’s add that in:


function locationUpdateHandler(pos, callback) { if (pos.coords) { console.log(“gps — location change occured: “ + pos.coords.longitude + “, “ + pos.coords.latitude); callback({ longitude: pos.coords.longitude, latitude: pos.coords.latitude, getAddress: function (callback) { Ti.Geolocation.reverseGeocoder(pos.coords.latitude, pos.coords.longitude, function (lookup) { if (lookup.success) { callback(lookup.places[0]); } else { callback({ success: false, error: “No location available” }); } }); } }); } else { console.log(“gps — No location coordinates available!”); } }

So with this function I’m doing a couple few things. When the handler is passed a position object from an event or location lookup, it returns to a callback the current longitude, latitude and a function called getAddress which the code within the callback can use (if required) to get the address.

Of course, you could just pass back the position object and handle any reverse geocoding outside of this module but my aim here was to try to encapsulate everything in a single module that would “just work.”

The final part is to add some code to kick-off location tracking:

exports.startGeoLocationTracking = function (callback) {

    function localHandler(pos) {
        locationUpdateHandler(pos, callback);
    }

    getLocationPermissions(function () {
        console.log(“gps — adding locaton handler”);
        Ti.Geolocation.addEventListener(“location”, localHandler);

            // add a stop tracking option
           exports.stopGeoLocationTracking = function () {
              console.log(“gps — stopping location tracking”);
              Ti.Geolocation.removeEventListener(“location”, localHandler);
           };
    }); 
};

With this function I’m exposing a method called startGeoLocationTracking which accepts a callback. Inside the function I’m getting location permissions and then passing results to locationUpdateHandler via a location function that allows me to a) easily remove it from the event and b) pass information back via a callback to the main application.

I’m also creating a new method to stop location tracking.

When thinking about how to do background location updates, it’s important to select the right method based on what your app is going to do. If you’re building a turn-by-turn navigation-based app or an app that’s tracking you, walking maybe point out points of interest along a specific route then it makes sense to use this level of accuracy and power.

There are several modes available for location tracking, so you can change the accuracy and activity to various methods all of which offer a range of accuracy vs power saving.

In iOS, if you’re looking for the best possible power-saving then there’s an option that can provide this called “significant location updates”—this works by using a combination of data sources to determine if the user’s location has updated, in a more battery efficient way.

Back to my recent app where I was trying to create a “Find My” type feature to locate nearby users. My approach to building this app was to do the following:

  1. The app uses a background location with significant location updates to ping the Longitude and latitude of the device location to a server, against the user/device record.
  2. Another user access the app looking for nearby users.
  3. The app calls the server sending its location and the server returns results of people within a specific radius.

Only one problem.

SignificantLocationUpdates is accurate to around 200-300m so it takes a BIG change for a location update to fire. This is awesome for battery life but terrible for this app which needs to be able to find people within 25m!

One solution was to not use significant location updates and use normal location modes instead, navigation mode (as set above) being the most accurate and battery draining.

Ordinarily, this is fine—and should work albeit with terrible battery life—the problem I had was the version of iOS at the time had a critical bug. When testing on the simulator everything was fine—I could see by turning on the background indicator that iOS was tracking the background location fine.

When testing on devices, however, I had a problem. There was an inconsistent experience where some devices would shut down the background location updates randomly and they wouldn’t restart. So one iPad, for example, lasted over 24 hours, but my iPhone would kill the app during the night.

After lots of googling, I discovered some Apple Radars (JIRA-like tickets) and determined it was an iOS bug that would be fixed in the 12.3 update—something I couldn’t wait for.

This forced me to go back to signification location updates and think of another solution that could work.

The problem I had was that I needed 25m accuracy with location updates that wouldn’t fire unless the user moved around a significant distance (100-200m)

The solution was one that required the use of both background location with significant location updates and push notifications.

My reasoning was that we didn’t update someone’s location constantly because it was ONLY important to know their location when they were being searched for—there’s no need to keep updating a server on someone’s position if no-one is looking for them.

So, I worked out that I could leave background location updates working with significant location updates so that it would update the user’s location if they moved 200-300m.

Next, I would update my code so that if someone was looking for nearby users, the server would look for users within a radius of 1km. (I wanted to pick a distance that would definitely capture people using significant location updates.

The server would then send a silent push notification to all users within 1km and when their app woke up to receive the push, the app would obtain a new, accurate location and submit this to the server.

Bingo. I had a system that could locate people within the accuracy required to use the minimal amount of power.

Ironically, because of a bug in iOS I’d created a way more efficient way for the app to do background location updates — even better the app would restart if the device restarted, or restart if the app crashed or iOS killed it. The only exception was that if the user explicitly kills the app (by swiping it away)—in these cases the app will not restart.

For that scenario, I used a scheduled local notification that I would schedule then cancel on a loop if the app was backgrounded—as long as the app was left in the background the location notification wouldn’t appear but if the app was killed, the notification wouldn’t be canceled and a local notification would remind the user to relaunch.

(If you’ve ever used the “Tile” app, and you force quit it you’ll see a similar thing happen after about 2-4 seconds).

In addition to all this, if the user didn’t relaunch the app OR the server didn’t receive updates for a while (say 24 hours), some code would send a push notification to users whose location hasn’t been updated in a while, to remind them to open the app.

To use the module in iOS, you use the following code:

var gps = require(“gps”);

gps.startGeoLocationTracking(function (e) {
    console.log(e.latitude);
    console.log(longitude);
);

To make all this easier to go through, understand and implement, I’ve put together a demo project that shows a map, adds annotations for location updates and implements all the code above, for iOS (there’s a sprinkling of Android-related code in there to prepare for a final Part 3 post coming soon). So take a look at the repo and play around with it.

When testing the iOS app, you may have to stop and start the GPS service using the button in the app if you change some settings like a visual indicator, etc. Also, if you use the simulator, be sure to go to the Debug menu and click Location > and set a location or select a ride like “Freeway drive” to see real-time location tracking in action.

Codestrong!

Learn more about creating custom tags in Titanium with Alloy.

2 COMMENTS

  1. Hi Jason… Great Article…

    The sample app throws some errors,

    First the Stop button:

    [ERROR] message = “gps.stopGeoLocationTracking is not a function. (In

    ‘gps.stopGeoLocationTracking()’, ‘gps.stopGeoLocationTracking’ is undefined)”;

    And when Start.. The App quits with the following:

    insert nil object from objects[5]”;

    I’m using Xcode 11.1 and SDK 8.2.1.GA

    Thanks!!

    • Thanks for the comment — I don’t get this which is odd — works fine on sim — will test on 8.2.1.GA as using 8.2.0.GA — could you log this as an issue in the repo and we can follow up there. Thanks!

LEAVE A REPLY

Please enter your comment!
Please enter your name here

This site uses Akismet to reduce spam. Learn how your comment data is processed.