Apple packed a lot of sensors into the small packages that are iOS devices. You as a developer can detect movement in three dimensions, acceleration, rotations, and impact and use those in your applications. All of this brought to your application in the CoreMotion framework.
CoreMotion has many uses. Game developers use it to make a phone into a gaming device where the phone’s motion controls the game play. Fitness developers use core motion to measure types of movements common to a sport on both the iPhone and Apple Watch. Using the sensors and Core motion could measure a swing of a baseball bat or golf club for example. Basic movements can also let other application execute a function. The Apple watch wakes up to a raise of the wrist for example.
Core motion is a complex framework to use, not only for coding reasons. As it deals with motion, a developer might need to know more about that motion before developing an application. The framework has some low-level objects for working directly with the sensors. There are, however, some motions that are so universal, core motion combines several sensors to get more bottom-line data. One good example of this kind of motion is putting one foot in front of the other. For walking and running motions CoreMotion has the CMPedometer
class. The class gives you direct data about the number of steps, To give you a taste of some of the issues involved in developing for Core motion, let’s make a pedometer from the CMPedometer
class. You’ll need to hook up a phone to Xcode since the simulator does not work with Core Motion.
Setting Up the Project
Create a new single-view project named CoreMotionPedometer. Use Swift as a language and a iPhone for a device. Save the project.
When the applications loads, change the Display Name to CMPedometer
This will give us a smaller name for the Icon. In this application you may need to test off of Xcode, and this will help you find your app better.
Core motion in general and the pedometer more specifically uses several motions that Apple considers private data. On a pedometer, you’re also using location data, so privacy is an even bigger issue. Developers need to add a key NSMotionUsageDescription
to the info.plist to ask permission of the user. You can add the key in XML like this:
<key>NSMotionUsageDescription</key> <string>This is a step counter, and we'd like to track motion. </string>
Most people will just go to the info.plist and add a entry to the dictionary.
In the drop down, select Privacy – Motion Usage Descriptor
This creates a key with a String
value. The string will be the body of an alert to accept the privacy settings for the pedometer. Add This is a step counter, and we’d like to track motion.
Designing the Storyboard
Go to the main storyboard. Add Five labels and a button. Place the button towards the bottom of the scene and the labels towards the top. Make the labels white, the button a green background and the scene background to black like this:
Layout the button on the bottom of the phone a quarter of the height of the device. Change the tile color for the Start button to White, and the title font to System Black 28pt. Using the auto layout pin tool , turn off the Constrain to Margins and set the left to 0, bottom to 0 and right to 0.
Add the three constraints. Control drag from the green of the button up to the black of the background.
When you release the mouse button, select Equal Heights in the menu that appears
The start button will fill the screen. Go to the size inspector. Find the Equal Height to Superview constraint, and click the Edit button there.
In the edit dialog that appears change the Multiplier to 1:4.
You’ll have a nice big button on the bottom.
I like having a big start/stop button. If you are doing a workout, it may be difficult to hit a small button, and this helps the user hit the button. It’s on the bottom in the thumb zone so the user can hit it one handed.
Select the Pedometer Demo label. In the attribute inspector, set the font to 22pt System heavy. Using the auto layout pin tool , turn Using the auto layout pin tool
, turn on the Constrain to Margins and set the Top to 20, Left to 0 and Right to 0. Add the constraints.
Select the other four labels. Set the font to System Heavy 17pt, Lines to 2 and Line Break to Word Wrap
You’ll be using two lines of text in this demo, one for a metric measurement and one for an Imperial measurement used in most sporting events. You’ll place them on two lines in the same label.
With all four labels selected, select the Stackview tool from the autolayout toolbar in the lower right. The labels will embed in a vertical stack view. Change the stack view to these attributes:
Use the pin tool to pin all four sides of the stack view 10 points. Update the constraints. You’ll have a storyboard like this one.
Close the attributes inspector and open the assistant editor. Control drag from the labels and make the following outlets, with the statusTitle
outlet to the Pedometer Demo label and the rest to their respective labels :
@IBOutlet weak var statusTitle: UILabel! @IBOutlet weak var stepsLabel: UILabel! @IBOutlet weak var avgPaceLabel: UILabel! @IBOutlet weak var paceLabel: UILabel! @IBOutlet weak var distanceLabel: UILabel!
Now add at the action. Control-drag from the button and add an action
@IBAction func startStopButton(_ sender: UIButton) { }
Some Preliminary Coding
Before we code the pedometer, I’m going to add a few properties and functions that will help us later. Close the assistant editor and go to the ViewController.swift file.
A very critical line goes just under the import UIKit
line, where we import core motion.
import CoreMotion
Then add these properties and constants.
let stopColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0) let startColor = UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0) // values for the pedometer data var numberOfSteps:Int! = nil var distance:Double! = nil var averagePace:Double! = nil var pace:Double! = nil
The constants will change the color of the stat stop button. You’ll store values in these for the properties of the pedometer object. Of course you need a pedometer object. Add this after these properties:
var pedometer = CMPedometer()
Pedometers don’t compute elapsed time. You might want that too in your pedometer. Add these two more properties to use a timer. :
// timers var timer = Timer() let timerInterval = 1.0 var timeElapsed:TimeInterval = 0.0
As you’ll find out, there’s more to this timer than just recording time.
You’ll need some more methods to handle some unit conversions you’ll do later, so add these:
//MARK: - Display and time format functions // convert seconds to hh:mm:ss as a string func timeIntervalFormat(interval:TimeInterval)-> String{ var seconds = Int(interval + 0.5) //round up seconds let hours = seconds / 3600 let minutes = (seconds / 60) % 60 seconds = seconds % 60 return String(format:"%02i:%02i:%02i",hours,minutes,seconds) } // convert a pace in meters per second to a string with // the metric m/s and the Imperial minutes per mile func paceString(title:String,pace:Double) -> String{ var minPerMile = 0.0 let factor = 26.8224 //conversion factor if pace != 0 { minPerMile = factor / pace } let minutes = Int(minPerMile) let seconds = Int(minPerMile * 60) % 60 return String(format: "%@: %02.2f m/s \n\t\t %02i:%02i min/mi",title,pace,minutes,seconds) } func computedAvgPace()-> Double { if let distance = self.distance{ pace = distance / timeElapsed return pace } else { return 0.0 } } func miles(meters:Double)-> Double{ let mile = 0.000621371192 return meters * mile }
The pedometer works in Metric, returning meters and meters per second. If you want other units such as a distance in miles, or a pace in minute per mile, you’ll need these to calculate the values. I convert times to strings in hh:mm:ss
here too.
Coding the Pedometer
With all these functions in place, your’e ready to code the pedometer. You’ll use the startStopButton
‘s action to toggle between starting and stopping the pedometer. Add this to the startStopButton
code
if sender.titleLabel?.text == "Start"{ //Start the pedometer //Toggle the UI to on state statusTitle.text = "Pedometer On" sender.setTitle("Stop", for: .normal) sender.backgroundColor = stopColor } else { //Stop the pedometer //Toggle the UI to off state statusTitle.text = "Pedometer Off: " sender.backgroundColor = startColor sender.setTitle("Start", for: .normal) }
You have button that toggles and appears correctly on the user interface. Below the Start the pedometer
comment add this:
pedometer = CMPedometer() pedometer.startUpdates(from: Date(), withHandler: { (pedometerData, error) in if let pedData = pedometerData{ self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)" } else { self.stepsLabel.text = "Steps: Not Available" } })
The first line of this code clears the pedometer by creating a new instance in the pedometer property. The startUpdates:FromDate:withHandler:
method starts sending updates from the pedometer. When an update occurs the handler within the closure executes. It checks of the pedometer is nil
. If nil
, the pedometer is not available. There are several other ways of checking this, but this is the most direct. If there is a value, the pedometer property number of steps is sent to the stepsLabel
‘s text
property.
To stop a pedometer, add the following under Stop the pedometer
comment.
pedometer.stopUpdates()
Run the Application
You are ready to run our first iteration. However, core motion only works on a real device, not the simulator. Plug your phone into a device, and select the device in the run schemes. I’ll assume you know how to do this and how to set up your device for testing. Run the application.
You’ll get a screen like this:
Press Start. You’ll get a screen asking for permission. Tap OK
Pick up your phone. Move your arms back and forth for 30 seconds like you are jogging. This is one of the motions that the pedometer uses to count steps. Press STOP.
While you see a few steps on the pedometer, you also see a lot of errors in the console Most of them you can ignore — they are errors related to internal caches you can’t touch. The very last error on the other hand is very important:
This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.
What’s happening here? Remember the code for updating is a closure, and runs on a separate thread from the main thread where UI updates happen. The closure tries to access the main thread when it really shouldn’t, because its timing is not the same as the main thread. This can crash the app. While this specifically mentions auto layout, I’d suggest never directly changing outlets from the closure to avoid any bad behavior.
Using Timer Loops
Instead of setting the labels in the closure, you set a property. Change this:
self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)"
to this:
self.numberOfSteps = Int(pedData.numberOfSteps)
The pedometer numberOfSteps
property is of type NSNumber
. You must covert it to an Int
to use in the ViewController
numberOfSteps
property.
You might think you can use a property observer to change the display. For example change the numberOfSteps
property to this:
// values for the pedometer data var numberOfSteps:Int! = nil{ didSet{ stepsLabel.text = "Steps:\(numberOfSteps)" } }
When the property changes, the label changes. You can run this code and do a simulated jog with your device. IF you do, you get that error message again. In code this is still updating the pedometer in the handler thread. You need a thread that has no problem updating to the view. That’s a timer.
Comment out the property observer:
// values for the pedometer data var numberOfSteps:Int! = nil /*{ //this does not work. didSet{ stepsLabel.text = "Steps:\(numberOfSteps)" } }*/
We earlier declared some timer properties. You’ll use that to set up a timer with these functions:
//MARK: - timer functions func startTimer(){ if timer.isValid { timer.invalidate() } timer = Timer.scheduledTimer(timeInterval: timerInterval,target: self,selector: #selector(timerAction(timer:)) ,userInfo: nil,repeats: true) } func stopTimer(){ timer.invalidate() displayPedometerData() } func timerAction(timer:Timer){ displayPedometerData() }
I discuss timers in more detail here. Basically startTimer
starts a timer with a 1 second interval that repeats. I’m being course here, you can set the interval to a finer resolution if you wish. Every second it calls the function timerAction
from the selector. In timerAction
I call a function displayPedometerData
I’ve yet to define which will display my pedometer data. The stopTimer
function shuts down the timer and updates the display one last time. Add the startTimer
and stopTimer
functions to the button action so it starts and stop the timer when the pedometer starts and stops.
@IBAction func startStopButton(_ sender: UIButton) { if sender.titleLabel?.text == "Start"{ //Start the pedometer pedometer = CMPedometer() startTimer() //start the timer pedometer.startUpdates(from: Date(), withHandler: { (pedometerData, error) in if let pedData = pedometerData{ self.numberOfSteps = Int(pedData.numberOfSteps) //self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)" } else { self.stepsLabel.text = "Steps: Not Available" } }) //Toggle the UI to on state statusTitle.text = "Pedometer On" sender.setTitle("Stop", for: .normal) sender.backgroundColor = stopColor } else { //Stop the pedometer pedometer.stopUpdates() stopTimer() // stop the timer //Toggle the UI to off state statusTitle.text = "Pedometer Off: " sender.backgroundColor = startColor sender.setTitle("Start", for: .normal) } }
Create a new function to update the view. For now we’ll update just the steps again.
func displayPedometerData(){ //Number of steps if let numberOfSteps = self.numberOfSteps{ stepsLabel.text = String(format:"Steps: %i",numberOfSteps) } }
I did two more things than I did with the property observer. I used an optional chain to unwrap numberOfSteps
, and then used as String initializer to format the string.
If you run the application and do your little in-place run, you’ll notice two differences: the step count updates faster than before and the error message disappears. We only have that CoreLocation
cache warning on the console. The timer thread indirectly updated the display separating the pedometer thread from the main thread.
Adding Elapsed Time
One advantage to a timer loop is we have a timer. Usually I use a higher resolution timer(0.1 seconds for example), but for this lesson I’ll leave it at a one second interval.
I can change the displayPedometerData
function to this:
func displayPedometerData(){ //Time Elapsed timeElapsed += self.timerInterval statusTitle.text = "On: " + timeIntervalFormat(interval: timeElapsed) //Number of steps if let numberOfSteps = self.numberOfSteps{ stepsLabel.text = String(format:"Steps: %i",numberOfSteps) } }
I increment the pedometer with a property timeElapsed
I created earlier. It keeps a count of the number of seconds elapsed since I started the pedometer. I display it using one of the formatting functions we added earlier that displays the time as hh:mm:ss.
To keep this time after you stop the timer, append the timeIntervalFormat function to the status title label
statusTitle.text = "Pedometer Off: " + timeIntervalFormat(interval: timeElapsed)
Build and run. Start the pedometer. You’ll get both a timer and step count now.
Stop the pedometer.
Adding Other Pedometer Properties
There’s several other properties of pedometers. I selected three more to show on our pedometer: current pace, Average pace and distance. Why you are getting that core location cache message makes sense now: the Pedometer checks your location repeatedly using CoreLocation. You have no control over that which is why I said to ignore the warning message. With that location data, the pedometer computes distance, and from the steps, distance, and it’s own timer pace and average pace.
All of these properties are optional. If the hardware or property is unavailable or nonexistent, the property returns nil
. However if you can’t get pace from a pedometer, you can compute the average pace from the distance and time. I made a function earlier computedAvgPace
that will compute an average pace or leave it as 0 if core location is not available.
To implement the other properties, change the startUpdates
closure to add the pedometer data to the viewController’s properties:
pedometer.startUpdates(from: Date(), withHandler: { (pedometerData, error) in if let pedData = pedometerData{ self.numberOfSteps = Int(pedData.numberOfSteps) //self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)" if let distance = pedData.distance{ self.distance = Double(distance) } if let averageActivePace = pedData.averageActivePace { self.averagePace = Double(averageActivePace) } if let currentPace = pedData.currentPace { self.pace = Double(currentPace) } } else { self.numberOfSteps = nil } })
Each pedometer property, if a number, is converted from NSNumber!
to a Double
for use in the classes, like we did for the integer numberOfSteps
.
In the displayPedometerData function, change it to this to include the other properties:
func displayPedometerData(){ timeElapsed += 1.0 statusTitle.text = "On: " + timeIntervalFormat(interval: timeElapsed) //Number of steps if let numberOfSteps = self.numberOfSteps{ stepsLabel.text = String(format:"Steps: %i",numberOfSteps) } //distance if let distance = self.distance{ distanceLabel.text = String(format:"Distance: %02.02f meters,\n %02.02f mi",distance,miles(meters: distance)) } else { distanceLabel.text = "Distance: N/A" } //average pace if let averagePace = self.averagePace{ avgPaceLabel.text = paceString(title: "Avg Pace", pace: averagePace) } else { avgPaceLabel.text = paceString(title: "Avg Comp Pace", pace: computedAvgPace()) } //pace if let pace = self.pace { paceLabel.text = paceString(title: "Pace:", pace: pace) } else { paceLabel.text = paceString(title: "Avg Comp Pace", pace: computedAvgPace()) } }
For each of these properties we optionally chain the property. A nil
vaule shows there was no reading for some reason. For the two pace strings, if we do not get a pace, I calculate the pace from the distance. I use the paceString
function defined in the conversion functions to make a string of both meters per second and minutes per mile.
Run again, and start the pedometer. Make the running motion and the device will begin to display data.
The Main Points for Core Motion
Core motion has a lot more to it, but this introduction give you some of the basics all core motion methods use. THis one is high level, the lower level, closer to the sensors require more tweaking than this virtually automatic method. However there are several points you should remember with Core Motion:
- Add the entry to the info.plist for security permissions
- Include the Core Motion LIbrary
- Check for availability of the device and the functions by looking for nil on properties.
- Don’t directly update outlets from a core motion closure
- Do indirectly update outlets from a timer loop.
The Whole Code
You’ll find all but the info.plist entry in this code. if you run this, make sure to include that. There is a download file coremotionpedometer with the completed project, including some app icons if you want to test off of Xcode. Go ahead, run a mile. It’s good for you.
ViewController.swift
// // ViewController.swift // CoreMotionPedometer // // Created by Steven Lipton on 2/10/17. // Copyright © 2017 Steven Lipton. All rights reserved. // import UIKit import CoreMotion class ViewController: UIViewController { //MARK: - Properties and Constants let stopColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0) let startColor = UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0) // values for the pedometer data var numberOfSteps:Int! = nil /*{ //this does not work. for demo purposes only. didSet{ stepsLabel.text = "Steps:\(numberOfSteps)" } }*/ var distance:Double! = nil var averagePace:Double! = nil var pace:Double! = nil //the pedometer var pedometer = CMPedometer() // timers var timer = Timer() var timerInterval = 1.0 var timeElapsed:TimeInterval = 1.0 //MARK: - Outlets @IBOutlet weak var statusTitle: UILabel! @IBOutlet weak var stepsLabel: UILabel! @IBOutlet weak var avgPaceLabel: UILabel! @IBOutlet weak var paceLabel: UILabel! @IBOutlet weak var distanceLabel: UILabel! @IBAction func startStopButton(_ sender: UIButton) { if sender.titleLabel?.text == "Start"{ //Start the pedometer pedometer = CMPedometer() startTimer() pedometer.startUpdates(from: Date(), withHandler: { (pedometerData, error) in if let pedData = pedometerData{ self.numberOfSteps = Int(pedData.numberOfSteps) //self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)" if let distance = pedData.distance{ self.distance = Double(distance) } if let averageActivePace = pedData.averageActivePace { self.averagePace = Double(averageActivePace) } if let currentPace = pedData.currentPace { self.pace = Double(currentPace) } } else { self.numberOfSteps = nil } }) //Toggle the UI to on state statusTitle.text = "Pedometer On" sender.setTitle("Stop", for: .normal) sender.backgroundColor = stopColor } else { //Stop the pedometer pedometer.stopUpdates() stopTimer() //Toggle the UI to off state statusTitle.text = "Pedometer Off: " + timeIntervalFormat(interval: timeElapsed) sender.backgroundColor = startColor sender.setTitle("Start", for: .normal) } } //MARK: - timer functions func startTimer(){ if timer.isValid { timer.invalidate() } timer = Timer.scheduledTimer(timeInterval: timerInterval,target: self,selector: #selector(timerAction(timer:)) ,userInfo: nil,repeats: true) } func stopTimer(){ timer.invalidate() displayPedometerData() } func timerAction(timer:Timer){ displayPedometerData() } // display the updated data func displayPedometerData(){ timeElapsed += 1.0 statusTitle.text = "On: " + timeIntervalFormat(interval: timeElapsed) //Number of steps if let numberOfSteps = self.numberOfSteps{ stepsLabel.text = String(format:"Steps: %i",numberOfSteps) } //distance if let distance = self.distance{ distanceLabel.text = String(format:"Distance: %02.02f meters,\n %02.02f mi",distance,miles(meters: distance)) } else { distanceLabel.text = "Distance: N/A" } //average pace if let averagePace = self.averagePace{ avgPaceLabel.text = paceString(title: "Avg Pace", pace: averagePace) } else { avgPaceLabel.text = paceString(title: "Avg Comp Pace", pace: computedAvgPace()) } //pace if let pace = self.pace { print(pace) paceLabel.text = paceString(title: "Pace:", pace: pace) } else { paceLabel.text = "Pace: N/A " paceLabel.text = paceString(title: "Avg Comp Pace", pace: computedAvgPace()) } } //MARK: - Display and time format functions // convert seconds to hh:mm:ss as a string func timeIntervalFormat(interval:TimeInterval)-> String{ var seconds = Int(interval + 0.5) //round up seconds let hours = seconds / 3600 let minutes = (seconds / 60) % 60 seconds = seconds % 60 return String(format:"%02i:%02i:%02i",hours,minutes,seconds) } // convert a pace in meters per second to a string with // the metric m/s and the Imperial minutes per mile func paceString(title:String,pace:Double) -> String{ var minPerMile = 0.0 let factor = 26.8224 //conversion factor if pace != 0 { minPerMile = factor / pace } let minutes = Int(minPerMile) let seconds = Int(minPerMile * 60) % 60 return String(format: "%@: %02.2f m/s \n\t\t %02i:%02i min/mi",title,pace,minutes,seconds) } func computedAvgPace()-> Double { if let distance = self.distance{ pace = distance / timeElapsed return pace } else { return 0.0 } } func miles(meters:Double)-> Double{ let mile = 0.000621371192 return meters * mile } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } }
Leave a Reply