Getting Device Motion with the CoreMotion Framework

As it does on iOS, the CoreMotion framework allows you to get access to live-updating data of the device’s motion through space. As the Apple Watch moves through space, its sensors update data and can call back into your apps. Getting access to this data is as easy as creating a CMMotionManager instance and registering for updates. Unfortunately, as of this writing, the class knows about many more data types than the watch actually supports. The CoreMotion classes on watchOS are analogous to the same classes on iOS, so they have references to data from three sensors: an accelerometer, a gyroscope, and a magnetometer. The data from these three sensors combines into an aggregate data type called simply device motion, which gives a more accurate measurement than the sensors alone. As you can discover yourself by calling accelerometerAvailable, gyroAvailable, and related methods, only the accelerometer is available on the Apple Watch.

You can take advantage of it for some cool effects. Open up the Soccer project that you made earlier and change it to use the accelerometer to move the ball.

Registering for Accelerometer Data Updates

One of the properties of the accelerometer is that it’s incredibly noisy, capable of producing data samples at an extremely fast rate. Processing every sample that comes out of it is like trying to drink out of a firehose. Instead of trying to read the raw data off the sensor manually, CoreMotion gives you an interface to register for updates. What this means is that you provide CoreMotion with the code to run whenever there’s new data (as well as a queue to run the code on), and it will take care of calling that code as new data streams in. Since this code will be called rapidly, you want to make sure it’s as fast as possible.

Open InterfaceController.swift in the Soccer WatchKit extension. You’ll register for the accelerometer data updates here. Before you can do anything, you’ll need to import the CoreMotion framework:

 import​ ​CoreMotion

The first thing you’ll do using CoreMotion is to add a new lazy property for a CMMotionManager that you’ll use to do the registration:

 lazy​ ​var​ motionManager = ​CMMotionManager​()

With that manager in hand, you’ll implement didAppear and start receiving accelerometer data there. Before you jump in, however, you need a place to store the received data and a queue to process it on. The accelerometer data comes in quickly, so you’ll want to process it on a background NSOperationQueue—processing it on the main queue would take up enough processor time that your UI would become unresponsive. Finally, in didDeactivate, you’ll be sure to stop collecting that data. Implement the methods as follows:

 var​ accelerometerData: ​CMAcceleration
 lazy​ ​var​ motionQueue = ​NSOperationQueue​()
 
 override​ ​func​ didAppear() {
 super​.didAppear()
 
 guard​ motionManager.accelerometerAvailable ​else​ { ​return​ }
 
  motionManager.startAccelerometerUpdatesToQueue(motionQueue) { data, _ ​in
 guard​ ​let​ data = data ​else​ { ​return​ }
 
 self​.accelerometerData = data.acceleration
  }
 }
 
 override​ ​func​ didDeactivate() {
 super​.didDeactivate()
 
 if​ motionManager.accelerometerActive {
  motionManager.stopAccelerometerUpdates()
  }
 }

The key to this method is the call to startAccelerometerUpdatesToQueue(_:withHandler:). The second parameter, passed here with Swift’s trailing closure syntax, is a closure that takes the data as an optional CMAccelerometerData instance, as well as an optional NSError. Inside the closure, which your motionManager will call on the motionQueue, you update the accelerometerData variable with the new data. Before using it, you’ll need to make sure it’s safe to access your accelerometer data from the main queue, where you’ll need to read it for the UI.

Working with Data Across Multiple Queues

When you created the accelerometerData earlier, you started writing to it directly from the accelerometer update closure. These accelerometer updates all happen on the motionQueue you created. If you’re writing to this variable on the motion queue while reading it from the main queue, bad things can happen. Let’s serialize your access to this data so you can stay safe. You’ll modify the declaration of accelerometerData to make it safer:

1: lazy​ ​var​ dataQueue = dispatch_queue_create(​"accelerometerData"​,
DISPATCH_QUEUE_CONCURRENT​)
private​ ​var​ _accelerometerData = ​CMAcceleration​(x: 0, y: 0, z: 0)
5: 
var​ accelerometerData: ​CMAcceleration
{
get​ {
var​ data: ​CMAcceleration​ = ​CMAcceleration​(x: 0, y: 0, z: 0)
10: 
dispatch_sync(dataQueue) {
data = ​self​._accelerometerData
}
15: return​ data
}
set​ {
dispatch_barrier_async(dataQueue) {
self​._accelerometerData = newValue
20:  }
}
}

First, on line 1, you create another type of queue. Instead of an NSOperationQueue, you use dispatch_queue_create to create a dispatch_queue_t instance. These queues, part of the Grand Central Dispatch APIs, are at a lower level than NSOperationQueue and will give you more control over how the queue is accessed. Next, on line 4, you create a private variable to store the accelerometer data. You’ll use this variable to store the actual data but use the accelerometerData variable without the underscore to access it.

To actually access the data inside the get for accelerometerData, you call dispatch_sync on line 11. This method executes the closure passed to it on the given queue, waiting to return until the closure finishes. Since you created the queue with the DISPATCH_QUEUE_CONCURRENT attribute, multiple closures can execute at once; that’s OK here because you’re not modifying the value of _accelerometerData, just reading it. When you want to write to it in the set method on line 18, you instead call dispatch_barrier_async. The “async” portion of the method means that this returns immediately—the caller of the setter does not need to wait for it to finish. The “barrier” portion is what makes this safe—instead of running concurrently while other methods are reading the data, this method causes the queue to finish all closures sent to it before the barrier, execute just the barrier closure, and then resume normal operations. In this way, you can guarantee that nothing will read your data while you’re writing to it but still allow for concurrent reads for things to be quick. This is a great general-purpose method of ensuring thread safety while still maintaining speed. Now that you’ve implemented it, you can put these values to use in the app’s UI.

Moving Objects with Motion

Until now, you haven’t looked at what’s contained inside your accelerometerData; you’ve just stored it as is. The CMAccelerometerData class, of which accelerometerData is an instance, has only one property, acceleration, which itself is simply a struct called CMAcceleration. The CMAcceleration struct has three values: x, y, and z. These values measure the acceleration of the watch along three axes, relative to the pull of gravity. The X axis goes horizontally along the surface of the watch’s screen, while the Y axis goes along the screen vertically. The Z axis goes perpendicular to the screen, coming out of it toward or away from you if you’re looking at the watch. The values of the acceleration measure these axes. If the x value is 1.0, for instance, then that’s indicative of a watch where the right side of the screen is facing down, because it’s being pulled exactly 1 G toward the Earth. Using these values, you can track the acceleration of your ball. Since this is a cumulative effect, you’ll store the ball’s speed and then add to it or subtract from it as the user moves his watch. Let’s add a variable for saving the speed:

 let​ maximumSpeed: ​Double​ = 5
 
 var​ ballSpeed: (x: ​Double​, y: ​Double​) = (0, 0) {
 didSet​ {
 if​ ballSpeed.x > maximumSpeed {
  ballSpeed.x = maximumSpeed
  }
 
 if​ ballSpeed.x < -maximumSpeed {
  ballSpeed.x = -maximumSpeed
  }
 
 if​ ballSpeed.y > maximumSpeed {
  ballSpeed.y = maximumSpeed
  }
 
 if​ ballSpeed.y < -maximumSpeed {
  ballSpeed.y = -maximumSpeed
  }
  }
 }

The maximumSpeed constant you defined ensures that the ball never moves too quickly; when you set the value of ballSpeed, you’ll clamp it to that maximum value. A positive speed moves the ball to the right or down, whereas a negative speed moves the ball up or to the left. Next, you need to use these values in the UI. You’ll be modifying the currentInsets property you created before, but you should add some additional logic to it to prevent the ball from leaving the screen:

 var​ currentInsets: ​UIEdgeInsets​ = ​UIEdgeInsetsZero​ {
 didSet​ {
 if​ currentInsets.​left​ + ballSize.width > soccerFieldSize.width {
  currentInsets.​left​ = soccerFieldSize.width - ballSize.width
  }
 
 if​ currentInsets.​left​ < 0 {
  currentInsets.​left​ = 0
  }
 
 if​ currentInsets.top + ballSize.height > soccerFieldSize.height {
  currentInsets.top = soccerFieldSize.height - ballSize.height
  }
 
 if​ currentInsets.top < 0 {
  currentInsets.top = 0
  }
  }
 }

Now that you’ve done that, the ball will stay onscreen at all times. Perfect! It’s time to use your updated data. You’ll use an NSTimer to periodically update the ball’s position. Create a new variable to store the timer:

 var​ updateTimer: ​NSTimer​?

Next, at the end of the didAppear method, you’ll create the timer, having it run 30 times per second:

 if​ updateTimer == ​nil​ {
  updateTimer = ​NSTimer​.scheduledTimerWithTimeInterval(1.0 / 30.0,
  target: ​self​,
  selector: ​"updateAccelerometerData:"​,
  userInfo: ​nil​,
  repeats: ​true​)
 }

When the timer runs, it will call a method named updateAccelerometerData(_:), so let’s write that next:

 func​ updateAccelerometerData(timer: ​NSTimer​) {
 let​ data = accelerometerData
 
  ballSpeed.x += data.x
  ballSpeed.y += -data.y
 
  currentInsets.​left​ += ​CGFloat​(ballSpeed.x)
  currentInsets.top += ​CGFloat​(ballSpeed.y)
 
  soccerField.setContentInset(currentInsets)
 }

This method applies the acceleration data from the accelerometer to the ball’s speed, speeding it up or slowing it down depending on the watch’s orientation. It then moves the ball according to its speed, repositioning it using the content inset of the soccer field, just as you had before. If you build and run the app on a watch device, you can move the ball around by moving your wrist! (Accelerometer data is not available on the watchOS simulator, so this sample must be run on a watch device.) Now that you can do that, you can eliminate the buttons in the UI. Open the storyboard and delete the buttons, making the field fill the screen vertically by setting its height to 1 relative to its container. Update the soccerFieldSize variable to return the entire screen:

 var​ soccerFieldSize: ​CGSize​ {
 return​ contentFrame.size
 }
images/Soccer-NoButtons.jpg

Build and run, and you’ll see the soccer field alone, with the ball moving around inside it, as shown in the figure.

Using the accelerometer data in this way opens up a new realm of possibilities for your apps. Whether you create a system for using the Apple Watch as a three-dimensional remote control, integrate it into a workout app, or just use the data to see how your user moves around, sensor data is a great resource. Hopefully, future versions of the Apple Watch will include new sensors, giving you even more accurate device positioning. Next, let’s look at another way the user’s motion can influence your app: starting workouts with HealthKit!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.42.5