Kodeco Forums

How To Make an App Like Runkeeper: Part 1

Runkeeper, a GPS app like the one youā€™re about to make, has over 40 million users! This tutorial will show you how to make an app like Runkeeper.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/553-how-to-make-an-app-like-runkeeper-part-1

iOS 10 absolutely fails to map points in background mode with Appleā€™s built-in GPS. However, that very same code works perfectly well with third-party GPS APIs like the one from Dual. So, has this been tested in background mode?

Again, I ask because background mode has never worked with the previous code or any mapping code that I have written that worked perfectly well in iOS versions prior to iOS 10. I know from previous threads and PM messages that Iā€™m not alone in this observation. I have repeatedly asked Apple about this issue but have only heard crickets.

Thanksā€¦

I didnā€™t test heavily on the device, though what testing I did did capture points when I backgrounded the app. There was jitter in the timing of the delivery of location updates to the delegate method but, at least in my limited testing, it didnā€™t lose any points.

(BTW, great tutorial. THANKS! Iā€™m just seeing the iOS 11 update now. I read a previous version and it was very helpful. Signed up just to share my experience with location manager in background mode.)

iOS 10 adds some additional requirements to get background location updatesā€¦

I was able to reliably get background updates in iOS 10 doing these things:

  1. add this key to your info.plist:

    <key>NSLocationAlwaysUsageDescription</key>
    <string>NSApp uses your location to keep the gub'mint informed of where you are at all times!</string>

    (If youā€™re going through Xcode, I think it calls this ā€œPrivacy - Location Always Usage Descriptionā€)

  2. In startLocationUpdates() add the line:

    locationManager.allowsBackgroundLocationUpdates = true

  3. Where the tutorial calls locationManager.requestWhenInUseAuthorization() you should instead call locationManager.requestAlwaysAuthorization()

Iā€™m being facetious about the message in info.plist! The point is, this message is displayed to the user with an Allow/Donā€™t Allow prompt. You should put some thought in to it. If a user canā€™t immediately see why they should click ā€œAllowā€ it, they will not do it and your app wonā€™t work. You get one shot at this.

The message is displayed at the time requestAlwaysAuthorization() is called. You should call it at a time where it will make immediate sense to the user why the app wants their locationā€¦ e.g., right after the user clicks a button like, ā€œstart tracking my hikeā€.

The message is displayed after the user has installed your app, but hasnā€™t yet been asked. After the user has answered, they wonā€™t be asked every time. If the user initially answers ā€œAllowā€ then something like a week later iOS will prompt the user to verify whether they want to continue to allow background tracking. So you really want to think about the message the user will see.

I added a reply about how to get background mode working in iOS 10, but I think I added it to the main thread rather than reply to your post. In case that doesnā€™t get a notification sent to you Iā€™m now replying to your post.

Anyone get this working with only Kilometers? I keep changing the UnitSpeed values and the Measurements to kilometers but on phones other than a spanish-speaking country it shows as meters.

Also, any way to keep the decimal points to 2 values only? everything returns .000

Edit: Got this working with Google Maps in case anyone is interested.

Hi
I am pretty new to iOS programming.
I have a problem even getting started here.
Why is the HomeViewController.swift completely empty??
Wouldnā€™t the segue at least be present in the file??
Thanks

Thank you for updates. Once iOS 11 goes GM, Iā€™ll revisit things and update to be sure itā€™s all working properly.

Iā€™m not sure I understand what youā€™re asking? Can you clarify please?

HomeViewController.swift isnā€™t actually needed at all; default UIViewController functionality is all that is required. I left it in the sample project to make it easier for readers should they decid to make an enhancement that would require it to exist.

The segues are defined, as always, in the storyboard. You only need to write prepare(for:sender:) if you actually need to gain control and execute some code (e.g. to inject model data into the destination view controller). In this app, the home screen is just a menu and pressing one of the buttons just displays the desired view. Each of the relevant view controllers it displays is completely standalone.

Hope this helps.

Hello, distance are showing in miles, but i put that as meters. How to change it to meters?

You should keep in mind that one mile has 1.6 kilometers. @rcritz Do you have any more feedback regarding this? Thank you - much appreciated! :]

What locale do you have set on your device?

I set Serbia Europe.

distance = Measurement(value: 0, unit: UnitLength.meters)

still show miles

Thanks for the quick response. When I set my locale to Serbia, I get meters (well, km actually) and not miles. Where are you seeing miles displayed (other than the Pace, which is expected to be in min/mi unless youā€™ve changed the code as described in part 1 of the tutorial)? I donā€™t recognize the screenshot you included.

Now it work, I set my locale in simulator to Serbia and it works. Thanks very much

Hi Richard
Iā€™ve been going through your code, trying to grasp what you have made, and I have a few questions I would appreciate if you would elaborate upon.

1: In the NewRunViewController you makeā€¦.
var run: Run?
This must be a variable of type Run-table in the SQLite database. I know the class is auto-generated when you create the tables, but I have not seen this way of working with SQLite in swift before ā€“ would you please point me in the right direction for instructions?

2: in func mapRegion:
A variable instantiation separated by a comma??? what is that?,
In the same functionI I understand run.distance, as ā€œdistanceā€ is a field in the run-table, but how can you call run.locations? is it because you have connected them in the inverse relationship??

private func mapRegion() ā†’ MKCoordinateRegion? {
guard
let locations = run.locations,
locations.count > 0
else {
return nil

}

3: In NewRunViewController I donā€™t understand the ā€œcontinueā€ in the else statement (last line)

  • wouldnā€™t that mean, that if the condition is not met with the guard, then youā€¦ well simply continue, and if that is the case, what is the purpose of guard?

extension NewRunViewController: CLLocationManagerDelegate {

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for newLocation in locations {
let howRecent = newLocation.timestamp.timeIntervalSinceNow
guard newLocation.horizontalAccuracy < 20 && abs(howRecent) < 10 else { continue }

Hi mutzo! Iā€™m happy to try and explain things:

  1. Actually, itā€™s Core Data. Yes, Core Data is using a SQLite store under the hood but app doesnā€™t use SQLite directly. Core Data is a massive topic and there are some great tutorials, both written and video, on the site to help get you up to speed on it. I especially recommend Luke Parhamā€™s ā€œBeginning Core Dataā€ video series.

  2. Those arenā€™t variable instantiations. The first is an optional binding (a Swift-specific language construct) and the second is just another clause in the guard statement. In Swift, you list all of your conditions in guard or if let statement separated by commas rather than having multiple, nested statements. That statement translates in English to ā€œmake sure run.locations is not nil, let me refer to it for the rest of this function as locations, and make sure there are some locations there. If any of that that is not true, return nil.ā€

  3. The continue applies to the containing for .. in loop. What it will do is skip a location if it is not sufficiently accurate or recent but will continue processing other locations in the update. A failed guard must exit at least itā€™s enclosing scope, which in this case is the for .. in loop. continue is a common way to ā€œexitā€ the loop for the current iteration and move on to the next.

Hope that helps.
-r

HI Mr. Critz:

When Iā€™m implementing the segue portion of the code in NewRunViewController, I get an error: ā€œOverriding ā€˜prepareā€™ must be as available as the declaration it overrides.ā€ All the necessary files associated with the application are accounted for, is there something that Iā€™m missing? Is this a Swift 3.0 syntax issue? Any help would be sincerely appreciated.

Thank you,
-Mark

Hey Mark!

I donā€™t see that error in my copy of the sources. The only way I know to trigger it is if you accidentally included a private on the declaration of prepare(for:sender:). In other words, if you did:

override private func prepare(for segue: UIStoryboardSegue, sender: Any?) { /* ... */ }

instead of:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) { /* ... */ }

Is that what happened?

Best,
-r