Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

How To Make an App Like RunKeeper with Swift: Part 1

$
0
0
Track distance and achievements as you build a run-tracking app!

Track distance and achievements as you build a run-tracking app!

With iOS 8, Apple introduced the new HealthKit API and corresponding Health App. Meanwhile, apps in the Health & Fitness category are hugely popular on the App Store.

This tutorial will show you how to make an app like RunKeeper, a GPS-based app to help you track your runs. Your new app, called MoonRunner, takes things to the next level with badges based on planets and moons in our Solar System!

Along the way, you’ll build all the features needed in a a motivational run-tracking app:

  • Core Location to track your route.
  • Shows a map during your run, with a constantly-updating line denoting your path.
  • Continually reports your average pace as you run.
  • Awards badges for running various distances.
  • Shows a map of your route when you’re finished.

You should already be familiar with the basics of Storyboards and Core Data before continuing. There’s so much to talk about that this tutorial comes in two parts: the first segment focuses on recording the run data and rendering the color-coded map, and the second segment introduces the badge system.

Getting Started

Begin by downloading the starter project download for the tutorial. Unzip it and open the project file, called MoonRunner.xcodeproj.

Build and run your project to check out what you’ll be starting with. The app will have a pretty simple flow:

  • A Home screen that presents a few app navigation options (three buttons).
  • A New Run screen where the user begins and records a new run (be sure to check the hidden UILabels).
  • A Run Details screen that shows the results, including the color-coded map.

Even a marathon begins with just a single CLLocationManager update. It’s time to start tracking some movement!

Starting the Run

You need to make a few important project-level changes first. Click on the MoonRunner project at the top of the project navigator. Select the MoonRunner target and then the Capabilities tab. Open up Background Modes, turn on the switch for this section on the right and then tick Location Updates. This will allow the app to update the location even if the user presses the home button to take a call, browse the net or find out where the nearest Starbucks is! Neat!

runkeeper_swift_part1_backgroundLocationUpdates_settings

Next, select the Info tab and open up Custom iOS Target Properties. Add these two lines to the plist:

NSLocationWhenInUseUsageDescription String MoonRunner wants to track your run
NSLocationAlwaysUsageDescription String MoonRunner wants to track your run

iOS will display the text in these two lines when it asks the user whether they want to allow the app to access the location data.

Note: If your app goes in the App Store, you’ll have to attach this disclaimer to your app’s description: “Continued use of GPS running in the background can dramatically decrease battery life.”

Now, back to the code. Open NewRunViewController.swift and add the following imports:

import CoreLocation
import HealthKit

You’ll need Core Location to access all the location-based APIs, and Health Kit to access units, quantities and conversion methods.

Then, at the end of the file, add a class extension to conform to the CLLocationManagerDelegate protocol:

// MARK: - CLLocationManagerDelegate
extension NewRunViewController: CLLocationManagerDelegate {
}

You’ll implement some delegate methods later, to be notified on location updates.

Next, add several new properties to the class:

var seconds = 0.0
var distance = 0.0
 
lazy var locationManager: CLLocationManager = {
  var _locationManager = CLLocationManager()
  _locationManager.delegate = self
  _locationManager.desiredAccuracy = kCLLocationAccuracyBest
  _locationManager.activityType = .Fitness
 
  // Movement threshold for new events
  _locationManager.distanceFilter = 10.0
  return _locationManager
}()
 
lazy var locations = [CLLocation]()
lazy var timer = NSTimer()

Note the syntax to lazily instantiate variables in Swift. Pretty straightforward, huh? Here is what these properties are here for:

  • seconds tracks the duration of the run, in seconds.
  • distance holds the cumulative distance of the run, in meters.
  • locationManager is the object you’ll tell to start or stop reading the user’s location.
  • locations is an array to hold all the Location objects that will roll in.
  • timer will fire each second and update the UI accordingly.

Let’s pause for a second to talk about your CLLocationManager and its configuration.
Once lazily instantiated, you set the CLLocationManagerDelegate to your NewRunViewController.

You then feed it a desiredAccuracy of kCLLocationAccuracyBest. Since you’re tracking a run, you want the best and most precise location readings, which also use more battery power.

The activityType parameter is made specifically for an app like this. It intelligently helps the device to save some power throughout a user’s run, say if they stop to cross a road.

Lastly, you set a distanceFilter of 10 meters. As opposed to the desiredAccuracy, this parameter doesn’t affect battery life. The desiredAccuracy is for readings, and the distanceFilter is for the reporting of readings.

As you’ll see after doing a test run later, the location readings can be a little zigged or zagged away from a straight line.

A higher distanceFilter could reduce the zigging and zagging and thus give you a more accurate line. Unfortunately, too high a filter would pixelate your readings. That’s why 10 meters is a good balance.

Next, add this line at the end of viewWillAppear(_:):

locationManager.requestAlwaysAuthorization()

This iOS 8 only method will request the location usage authorization from your users. If you want your app to run on iOS versions prior to 8, you will need to test the availability of this method before calling it.

Now add the following method to your implementation:

override func viewWillDisappear(animated: Bool) {
  super.viewWillDisappear(animated)
  timer.invalidate()
}

With this method, the timer is stopped when the user navigates away from the view.

Add the following method:

func eachSecond(timer: NSTimer) {
  seconds++
  let secondsQuantity = HKQuantity(unit: HKUnit.secondUnit(), doubleValue: seconds)
  timeLabel.text = "Time: " + secondsQuantity.description
  let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: distance)
  distanceLabel.text = "Distance: " + distanceQuantity.description
 
  let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
  let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: seconds / distance)
  paceLabel.text = "Pace: " + paceQuantity.description
}

This is the method that will be called every second, by using an NSTimer (which will be set up shortly). Each time this method is called, you increment the second count and update each of the statistics labels accordingly.

Here’s the final helper method to add to the class before starting your run:

func startLocationUpdates() {
  // Here, the location manager will be lazily instantiated
  locationManager.startUpdatingLocation()
}

Here, you tell the manager to start getting location updates! Time to hit the pavement. Wait! Are you lacing up your shoes? Not that kind of run!

To actually begin the run, add these lines to the end of startPressed(_:):

seconds = 0.0
distance = 0.0
locations.removeAll(keepCapacity: false)
timer = NSTimer.scheduledTimerWithTimeInterval(1,
  target: self,
  selector: "eachSecond:",
  userInfo: nil,
  repeats: true)
startLocationUpdates()

Here, you’re resetting all the fields that will update continually throughout the run and then starting up the timer and location updates.

Build and run. If you start a new run you will see the time label increment.

moonrunner-run1

However, the distance and pace labels are not updated because you are not tracking the location yet. So, let’s do this now!

Recording the Run

You’ve created the CLLocationManager, but now you need to get updates from it. That is done through its delegate. Open NewRunViewController.swift once again and add the following method to the class extension conforming to CLLocationManagerDelegate:

func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
  for location in locations as! [CLLocation] {
    if location.horizontalAccuracy < 20 {
      //update distance
      if self.locations.count > 0 {
        distance += location.distanceFromLocation(self.locations.last)
      }
 
      //save location
      self.locations.append(location)
    }
  }
}

This delegate method will be called each time there are new location updates to provide the app. Usually, the locations array only contains one object, but if there are more, they are ordered by time with the most recent location last.

A CLLocation contains some great information. Namely the latitude and longitude, along with the timestamp of the reading.

But before blindly accepting the reading, it’s worth a horizontalAccuracy check. If the device isn’t confident it has a reading within 20 meters of the user’s actual location, it’s best to keep it out of your dataset. This check is especially important at the start of the run, when the device first starts narrowing down the general area of the user. At that stage, it’s likely to update with some inaccurate data for the first few points.

If the CLLocation passes the check, then you add the distance between it and the most recent point to the cumulative distance of the run. The distanceFromLocation(_:) method is very convenient here, taking into account all sorts of surprisingly-difficult math involving the Earth’s curvature.

Finally, you append the location object itself to a growing array of locations.

Note: The CLLocation object also contains information on altitude, with a corresponding verticalAccuracy value. As every runner knows, hills can be a game changer on any run, and altitude can affect the amount of oxygen available. A challenge to you, then, is to think of a way to incorporate this data into the app.

Send the Simulator on a Run

As much as I hope that this tutorial and the apps you build encourage more enthusiasm for fitness, the build and run phase does not need to be taken that literally while developing it!

You don’t need to lace up and head out the door either, for there’s a way to get the simulator to pretend it’s running!

Build and run in the simulator, and select Debug\Location\City Run to have the simulator start generating mock data:

runkeeper_swift_part1_simulatorRun

Of course, this is much easier and less exhausting than taking a short run to test this — or any other — location-based app.

However, I recommend eventually doing a true beta test with a device. Doing so gives you the chance to fine-tune the location manager parameters and assess the quality of location data you can really get.

Thorough testing could help instill a healthy habit, too. :]

Saving the Run

At some point, despite that voice of motivation inside you that tells you to keep going (mine sounds like Gunnery Sergeant Hartman in Full Metal Jacket), there comes a time to end the run. You already arranged for the UI to accept this input, and now it’s time to process that data.

Add this method to NewRunViewController.swift:

func saveRun() {
  // 1
  let savedRun = NSEntityDescription.insertNewObjectForEntityForName("Run",
    inManagedObjectContext: managedObjectContext!) as! Run
  savedRun.distance = distance
  savedRun.duration = seconds
  savedRun.timestamp = NSDate()
 
  // 2
  var savedLocations = [Location]()
  for location in locations {
    let savedLocation = NSEntityDescription.insertNewObjectForEntityForName("Location",
      inManagedObjectContext: managedObjectContext!) as! Location
    savedLocation.timestamp = location.timestamp
    savedLocation.latitude = location.coordinate.latitude
    savedLocation.longitude = location.coordinate.longitude
    savedLocations.append(savedLocation)
  }
 
  savedRun.locations = NSOrderedSet(array: savedLocations)
  run = savedRun
 
  // 3
  var error: NSError?
  let success = managedObjectContext!.save(&error)
  if !success {
    println("Could not save the run!")
  }
}

So what’s happening here? If you’ve done a simple Core Data flow before, this should look like a familiar way to save new objects:

1. You create a new Run object, and give it the cumulative distance and duration values as well as assign it a timestamp.
2. Each of the CLLocation objects recorded during the run is trimmed down to a new Location object and saved. Then you link the locations to the Run
3. You then save your NSManagedObjectContext

Finally, you’ll need to call this method when the user stops the run and then chooses to save it. Find actionSheet(_:clickedButtonAtIndex:) and add the following line to the top the if buttonIndex == 1 block, above the call to performSegueWithIdentifier(_:sender:):

saveRun()

Build and run. You are now able to record a run and save it.

moonrunner-run2

However, the detail view of the run is mostly empty. That’s coming up next!

Revealing the Map

Now it’s time to show the map post-run stats. Open DetailViewController.swift and import HealthKit:

import HealthKit

Next, find configureView() and add the following code to the method:

let distanceQuantity = HKQuantity(unit: HKUnit.meterUnit(), doubleValue: run.distance.doubleValue)
distanceLabel.text = "Distance: " + distanceQuantity.description
 
let dateFormatter = NSDateFormatter()
dateFormatter.dateStyle = .MediumStyle
dateLabel.text = dateFormatter.stringFromDate(run.timestamp)
 
let secondsQuantity = HKQuantity(unit: HKUnit.secondUnit(), doubleValue: run.duration.doubleValue)
timeLabel.text = "Time: " + secondsQuantity.description
 
let paceUnit = HKUnit.secondUnit().unitDividedByUnit(HKUnit.meterUnit())
let paceQuantity = HKQuantity(unit: paceUnit, doubleValue: run.duration.doubleValue / run.distance.doubleValue)
paceLabel.text = "Pace: " + paceQuantity.description

This sets up the details of the run into the three labels on the screen.

Rendering the map will require just a little more detail. There are three basic steps to it:

  1. First, the region needs to be set so that only the run is shown and not the entire world!
  2. Then the line drawn over the top to indicate where the run went needs to be created.
  3. Finally, you’ll add some styling to the line to indicate the speed for particular sections of the run.

Start by adding the following method to the class:

func mapRegion() -> MKCoordinateRegion {
  let initialLoc = run.locations.firstObject as! Location
 
  var minLat = initialLoc.latitude.doubleValue
  var minLng = initialLoc.longitude.doubleValue
  var maxLat = minLat
  var maxLng = minLng
 
  let locations = run.locations.array as! [Location]
 
  for location in locations {
    minLat = min(minLat, location.latitude.doubleValue)
    minLng = min(minLng, location.longitude.doubleValue)
    maxLat = max(maxLat, location.latitude.doubleValue)
    maxLng = max(maxLng, location.longitude.doubleValue)
  }
 
  return MKCoordinateRegion(
    center: CLLocationCoordinate2D(latitude: (minLat + maxLat)/2,
                                   longitude: (minLng + maxLng)/2),
    span: MKCoordinateSpan(latitudeDelta: (maxLat - minLat)*1.1,
                           longitudeDelta: (maxLng - minLng)*1.1))
}

An MKCoordinateRegion represents the display region for the map, and you define it by supplying a center point and a span that defines horizontal and vertical ranges.

For example, my jog may be quite zoomed in around my short route, while my more athletic friend’s map will appear more zoomed out to cover all the distance she traveled. It’s important to also account for a little padding, so that the route doesn’t crowd all the way to the edge of the map.

Next, add the following method:

func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
  if !overlay.isKindOfClass(MKPolyline) {
    return nil
  }
 
  let polyline = overlay as! MKPolyline
  let renderer = MKPolylineRenderer(polyline: polyline)
  renderer.strokeColor = UIColor.blackColor()
  renderer.lineWidth = 3
  return renderer
}

This method says that whenever the map comes across a request to add an overlay, it should check if it’s an MKPolyline. If so, it should use a renderer that will make a black line. You’ll spice this up shortly. An overlay is something that is drawn on top of a map view. A polyline is such an overlay and represents a line drawn from a series of location points.

Lastly, you need to define the coordinates for the polyline. Add the following method:

func polyline() -> MKPolyline {
  var coords = [CLLocationCoordinate2D]()
 
  let locations = run.locations.array as! [Location]
  for location in locations {
    coords.append(CLLocationCoordinate2D(latitude: location.latitude.doubleValue,
      longitude: location.longitude.doubleValue))
  }
 
  return MKPolyline(coordinates: &coords, count: run.locations.count)
}

Here, you shove the data from the Location objects into an array of CLLocationCoordinate2D, the required format for polylines.

Now, it’s time to put these three together! Add the following method:

func loadMap() {
  if run.locations.count > 0 {
    mapView.hidden = false
 
    // Set the map bounds
    mapView.region = mapRegion()
 
    // Make the line(s!) on the map
    mapView.addOverlay(polyline())
  } else {
    // No locations were found!
    mapView.hidden = true
 
    UIAlertView(title: "Error",
              message: "Sorry, this run has no locations saved",
             delegate:nil,
    cancelButtonTitle: "OK").show()
  }
}

Here, you make sure that there are points to draw, set the map region as defined earlier, and add the polyline overlay.

Finally, add the following code at the end of configureView():

loadMap()

And now build and run!. You should now see a map after your simulator is done with its workout.

Notice how the default debug location happens to be next door to Apple :]

Notice how the default debug location happens to be next door to Apple :]

Finding the Right Color

The app is pretty cool as-is, but one way you can help your users train even smarter is to show them how fast or slow they ran at each leg of the run. That way, they can identify areas where they are most at risk of straying from an even pace.

To do this, you’ll extend the polyline class you’ve already been using to add color support.

In Xcode, navigate to File\New\File…, and select iOS\Source\Swift File. Call the file MulticolorPolylineSegment and save it to disk. When the file opens in the editor, replace its contents with the following:

import UIKit
import MapKit
 
class MulticolorPolylineSegment: MKPolyline {
  var color: UIColor?
}

This special, custom, polyline will be used to render each segment of the run. The color is going to denote the speed and so the color of each segment is stored here on the polyline. Other than that, it’s the same as an MKPolyline. There will be one of these objects for each segment connecting two locations.

Next, you need to figure out how to assign the right color to the right polyline segment. Add the following class method to your MulticolorPolylineSegment class:

private class func allSpeeds(forLocations locations: [Location]) ->
 (speeds: [Double], minSpeed: Double, maxSpeed: Double) {
  // Make Array of all speeds. Find slowest and fastest
  var speeds = [Double]()
  var minSpeed = DBL_MAX
  var maxSpeed = 0.0
 
  for i in 1..<locations.count {
    let l1 = locations[i-1]
    let l2 = locations[i]
 
    let cl1 = CLLocation(latitude: l1.latitude.doubleValue, longitude: l1.longitude.doubleValue)
    let cl2 = CLLocation(latitude: l2.latitude.doubleValue, longitude: l2.longitude.doubleValue)
 
    let distance = cl2.distanceFromLocation(cl1)
    let time = l2.timestamp.timeIntervalSinceDate(l1.timestamp)
    let speed = distance/time
 
    minSpeed = min(minSpeed, speed)
    maxSpeed = max(maxSpeed, speed)
 
    speeds.append(speed)
  }
 
  return (speeds, minSpeed, maxSpeed)
}

This method returns the array of speed values for each sequential pair of locations, along with the minimum and maximum speeds. To return multiple values, you wrap them in a tuple.

Note: For more on tuples and how to use them as return types, check the Swift Documentation on Tuples and scroll down to the section named Functions with Multiple Return Values.

The first thing you’ll notice is a loop through all the locations from the input. You have to convert each Location to a CLLocation so you can use distanceFromLocation(_:).

Remember basic physics: distance divided by time equals speed. Each location after the first is compared to the one before it, and by the end of the loop you have a complete collection of all the changes in speed throughout the run.

This method is private, and can only be accessed from within the class. Next, add the following class method that will act as the public interface:

class func colorSegments(forLocations locations: [Location]) -> [MulticolorPolylineSegment] {
  var colorSegments = [MulticolorPolylineSegment]()
 
  // RGB for Red (slowest)
  let red   = (r: 1.0, g: 20.0 / 255.0, b: 44.0 / 255.0)
 
  // RGB for Yellow (middle)
  let yellow = (r: 1.0, g: 215.0 / 255.0, b: 0.0)
 
  // RGB for Green (fastest)
  let green  = (r: 0.0, g: 146.0 / 255.0, b: 78.0 / 255.0)
 
  let (speeds, minSpeed, maxSpeed) = allSpeeds(forLocations: locations)
 
  // now knowing the slowest+fastest, we can get mean too
  let meanSpeed = (minSpeed + maxSpeed)/2
 
  return colorSegments
}

Here you define the three colors you’ll use for slow, medium and fast polyline segments.

Each color, in turn, has its own RGB components. The slowest components will be completely red, the middle will be yellow, and the fastest will be green. Everything else will be a blend of the two nearest colors, so the end result could be quite colorful.

Notice how you also retrieve and decompose the return tuple from allSpeeds(forLocations:) and then use minSpeed and maxSpeed to compute a meanSpeed.

Finally, add the following to the end of the method, before the return statement:

for i in 1..<locations.count {
  let l1 = locations[i-1]
  let l2 = locations[i]
 
  var coords = [CLLocationCoordinate2D]()
 
  coords.append(CLLocationCoordinate2D(latitude: l1.latitude.doubleValue, longitude: l1.longitude.doubleValue))
  coords.append(CLLocationCoordinate2D(latitude: l2.latitude.doubleValue, longitude: l2.longitude.doubleValue))
 
  let speed = speeds[i-1]
  var color = UIColor.blackColor()
 
  if speed < minSpeed { // Between Red & Yellow
    let ratio = (speed - minSpeed) / (meanSpeed - minSpeed)
    let r = CGFloat(red.r + ratio * (yellow.r - red.r))
    let g = CGFloat(red.g + ratio * (yellow.g - red.g))
    let b = CGFloat(red.r + ratio * (yellow.r - red.r))
    color = UIColor(red: r, green: g, blue: b, alpha: 1)
  }
  else { // Between Yellow & Green
    let ratio = (speed - meanSpeed) / (maxSpeed - meanSpeed)
    let r = CGFloat(yellow.r + ratio * (green.r - yellow.r))
    let g = CGFloat(yellow.g + ratio * (green.g - yellow.g))
    let b = CGFloat(yellow.b + ratio * (green.b - yellow.b))
    color = UIColor(red: r, green: g, blue: b, alpha: 1)
  }
 
  let segment = MulticolorPolylineSegment(coordinates: &coords, count: coords.count)
  segment.color = color
  colorSegments.append(segment)
}

In this loop, you determine the value of each pre-calculated speed, relative to the full range of speeds. This ratio then determines the UIColor to apply to the segment.

Next, you construct a new MulticolorPolylineSegment with the two coordinates and the blended color.
Finally, you collect all the multicolored segments together, and you’re almost ready to render!

Applying the Multicolored Segments

Repurposing the detail view controller to use your new multicolor polyline is actually quite simple! Open DetailViewController.swift, find loadMap(), and replace the following line:

mapView.addOverlay(polyline())

with the following:

let colorSegments = MulticolorPolylineSegment.colorSegments(forLocations: run.locations.array as! [Location])
mapView.addOverlays(colorSegments)

This creates the array of segments and adds all the overlays to the map.

Lastly, you need to prepare your polyline renderer to pay attention to the specific color of each segment. So replace your current implementation of mapView(_:rendererForOverlay:) with the following:

func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
  if !overlay.isKindOfClass(MulticolorPolylineSegment) {
    return nil
  }
 
  let polyline = overlay as! MulticolorPolylineSegment
  let renderer = MKPolylineRenderer(polyline: polyline)
  renderer.strokeColor = polyline.color
  renderer.lineWidth = 3
  return renderer
}

This is very similar to what you had before, but now the specific color of each segment renders individually.

Alright! Now you’re all set to build & run, let the simulator go on a little jog, and check out the fancy multi-colored map afterward!

runkeeper_swift_part1_coloredPolylineMap

Leaving a Trail Of Breadcrumbs

That post-run map is stunning, but how about one during the run?

Note: The Breadcrumb sample project from Apple has some similar functionality, if you want to see another implementation.

Open Main.storyboard and find the New Run Scene. Drag in a new MapKit View and size it roughly so it fits between the “Ready to Launch” label and the Start button.

runkeeper_swift_part1_MKMapView_IBOutlet

Then, be sure to add the four Auto Layout constraints displayed in the screenshots:

  • Top Space to Ready to Launch? label of 20 points.
  • Bottom Space to Start! button of 20 points.
  • Trailing Space and Leading Space of 0 points to the Superview.

Then open NewRunViewController.swift and add the MapKit import:

import MapKit

Next, add an outlet for the map to the class:

@IBOutlet weak var mapView: MKMapView!

Then add this line to the end of viewWillAppear(_:):

mapView.hidden = true

This makes sure that the map is hidden at first. Now add this line to the end of startPressed(_:):

mapView.hidden = false

This makes the map appear when the run starts.

The trail is going to be another polyline, which means you’ll need to implement a map delegate method. Add the following class extension to the end of the file:

// MARK: - MKMapViewDelegate
extension NewRunViewController: MKMapViewDelegate {
  func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer! {
    if !overlay.isKindOfClass(MKPolyline) {
      return nil
    }
 
    let polyline = overlay as! MKPolyline
    let renderer = MKPolylineRenderer(polyline: polyline)
    renderer.strokeColor = UIColor.blueColor()
    renderer.lineWidth = 3
    return renderer
  }
}

This version is similar to the one for the run details screen, except that the stroke color is always blue here.

Next, you need to write the code to update the map region and draw the polyline every time a valid location is found. Find your current implementation of locationManager(_:didUpdateLocations:) and update it to this:

func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
  for location in locations as! [CLLocation] {
    let howRecent = location.timestamp.timeIntervalSinceNow
 
    if abs(howRecent) < 10 && location.horizontalAccuracy < 20 {
      //update distance
      if self.locations.count > 0 {
        distance += location.distanceFromLocation(self.locations.last)
 
        var coords = [CLLocationCoordinate2D]()
        coords.append(self.locations.last!.coordinate)
        coords.append(location.coordinate)
 
        let region = MKCoordinateRegionMakeWithDistance(location.coordinate, 500, 500)
        mapView.setRegion(region, animated: true)
 
        mapView.addOverlay(MKPolyline(coordinates: &coords, count: coords.count))
      }
 
      //save location
      self.locations.append(location)
    }
  }
}

Now, the map always centers on the most recent location, and constantly adds little blue polylines to show the user’s trail thus far.

Open Main.storyboard and find the New Run Scene. Connect the outlet for mapView to the map view, and set its delegate to the view controller.

Build and run, and start a new run. You’ll see the map updating in real-time!

runkeeper_swift_part1_NewRunMap

Is your heart rate up yet? :]

Where To Go From Here

Here’s a download of the sample project with all the code you’ve written up until this point.

You’ve seen how to store data for a run in a simple set of Core Data models, and display the run details (even in real time!) on a map. That’s really the core of MoonRunner, so congratulations!

If you’re up for some super-extra credit challenges, why not try to use the altitude information, maybe to change the thickness of the line segments? Or try blending the segment colors more smoothly by averaging a segment’s speed with that of the segment before it.

In any case, stay tuned for part two of this tutorial, where you’ll add the final touches and introduce a badge system personalized for each user.

As always, feel free to post comments and questions in the forum discussion below!

The post How To Make an App Like RunKeeper with Swift: Part 1 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles