Drawing Data

Create a new file with the shortcut N and choose the Cocoa Touch Class template. In the Class text field, type in the name DiagramView and make it a subclass of UIView. Click Next and then click Create.

Xcode creates a subclass of UIView and fills it with some comments to get us started. Remove the lines with the comments and the multiline comment strings such that the class looks like this:

 class​ ​DiagramView​: ​UIView​ {
 override​ ​func​ ​draw​(_ rect: ​CGRect​) {
  }
 }

UIKit calls the draw method of a UIView subclass whenever the view is rendered on-screen. We have to put our drawing code into this method. But before we can draw data, we need to have it available in the view class. Add the following property to DiagramView:

 class​ ​DiagramView​: ​UIView​ {
 
»var​ dataArray: [​AccelerationData​] = []
 
 override​ ​func​ ​draw​(_ rect: ​CGRect​) {
  }
 }

Next, add the highlighted code to the draw(_:) method:

 override​ ​func​ ​draw​(_ rect: ​CGRect​) {
 
»let​ width = frame.size.width
»let​ height = frame.size.height
»let​ y0 = height/2.0
»
»var​ maximum = 0.0
»for​ data ​in​ dataArray {
» maximum = ​max​(maximum, ​abs​(data.value))
» }
 }

The view doesn’t know about acceleration or time frames, but it knows the frame it needs to draw. To be able to scale the acceleration data and the time values to the available space in the view, we assign the width and the height of the view and the maximum of all values to constants. In addition, we store half the height to use it later as the vertical zero line.

The data will be drawn using an instance of UIBezierPath, which is a path consisting of line segments that we can render in our custom views. Add the highlighted code to draw(_:):

 override​ ​func​ ​draw​(_ rect: ​CGRect​) {
 
 let​ width = frame.size.width
 let​ height = frame.size.height
 let​ y0 = height/2.0
 
 var​ maximum = 0.0
 for​ data ​in​ dataArray {
  maximum = ​max​(maximum, ​abs​(data.value))
  }
 
»let​ bezierPath = ​UIBezierPath​()
»
»guard​ ​let​ firstPoint = dataArray.first,
»let​ lastPoint = dataArray.last,
» maximum > 0 ​else​ {
»return
» }
»let​ scale = height / (​CGFloat​(maximum) * 2.0)
»let​ y = y0 + ​CGFloat​(firstPoint.value) * scale
» bezierPath.​move​(to: ​CGPoint​(x: 0, y: y))
 }

The properties first and last defined on Array return the first and the last element, respectively. If the array is empty, these values are nil; if the array has only one element, first and last both return this element. To scale the values in the diagram for optimal presentation, we’ll divide by maximum. Dividing by zero is mathematically not allowed, so we need to make sure that we proceed only when maximum is greater than zero.

Next we calculate the scale for the acceleration values. Because the zero line is a horizontal line through the center of the view, we have to scale the values to half the height of the view. Then we calculate the first value using the zero value y0 and the scaled value of the data point. We add this value to the Bézier path using the move(to:) method. Think about this like drawing with a pencil. Before we draw the first segment, we have to move the pencil to the location where the segment should start.

Now add the highlighted lines to the end of draw(_:):

 override​ ​func​ ​draw​(_ rect: ​CGRect​) {
 
 // ...
 // define constrants
 // ...
 
 let​ bezierPath = ​UIBezierPath​()
 
 guard​ ​let​ firstPoint = dataArray.first,
 let​ lastPoint = dataArray.last,
  maximum > 0 ​else​ {
 return
  }
 let​ scale = height / (​CGFloat​(maximum) * 2.0)
 let​ y = y0 + ​CGFloat​(firstPoint.value) * scale
  bezierPath.​move​(to: ​CGPoint​(x: 0, y: y))
 
»let​ totalTime = lastPoint.timestamp - firstPoint.timestamp
»if​ totalTime == 0 {
»return
» }
»for​ dataPoint ​in​ dataArray {
»let​ timeDiff = dataPoint.timestamp - firstPoint.timestamp
»let​ x = ​CGFloat​(timeDiff / totalTime) * width
»let​ y = y0 + ​CGFloat​(dataPoint.value) * scale
» bezierPath.​addLine​(to: ​CGPoint​(x: x, y: y))
» }
 }

In this code we first calculate the total time frame of the collected data. This value will be used to scale the time values on the x-axis such that they span the whole width of the view. Because we’re going to divide by the total time, we need to make sure that the value is not zero.

Then, for each data point, we calculate the x- and y-values for the corresponding point in the Bézier path. The x-value is the time difference to the first data point scaled to the width of the view using the total time frame. We calculate the y-value as we did for the first point. The result is added to the Bézier path using the addLine(to:) method.

The Bézier path is now complete and we can draw it with the highlighted lines in the following code:

 override​ ​func​ ​draw​(_ rect: ​CGRect​) {
 
 // ...
 // define constrants
 // ...
 
 // ...
 // setup bezier path
 // ...
 
»UIColor​.label.​setStroke​()
» bezierPath.lineWidth = 1
» bezierPath.​stroke​()
 }

The code sets the stroke color and the line width for the Bézier path and then draws it by calling stroke. The color label should be used for static text and related elements. We use it here because this way the drawn line is visible in dark and light mode.

Now we can use the diagram view in the storyboard. Open Main.storyboard, select the blank view in the stack view, and open the identity inspector with the shortcut 4. Type DiagramView into the Class text field and hit return.

To update the diagram view when new acceleration data is available, first we need a reference to it in the view controller. If it’s not already open, open the assistant editor using the shortcut . Press and hold control and drag a connection from the view into the code below the xAccelerationData property:

images/Sensors/x_diagram_view_connection.png

Make sure the connection is set to Outlet, type in the name xDiagramView, and click Connect.

Xcode adds the highlighted property to MeasurementViewController:

 let​ motionManager = ​CMMotionManager​()
 var​ xAccelerationData: [​AccelerationData​] = []
»@IBOutlet​ ​var​ xDiagramView: ​DiagramView​!

Next, replace the declaration of xAccelerationData with the following code:

 var​ xAccelerationData: [​AccelerationData​] = [] {
 didSet​ {
  xDiagramView.dataArray = xAccelerationData
  }
 }

With this code we add a didSet observer to the xAccelerationData property. The didSet observer of a property is called immediately after a new value is assigned. In the observer, we pass the collected acceleration data to the diagram view.

Now we need to trigger the drawing of the data in the diagram view when new data arrives. Open DiagramView and replace the declaration of the dataArray property with the following code:

 var​ dataArray: [​AccelerationData​] = [] {
 didSet​ {
 setNeedsDisplay​()
  }
 }

We use a didSet observer to tell the view that it should draw itself by calling setNeedsDisplay. This triggers a call of the draw(_:) method.

Build and run the application on your iPhone, tap the Start button, and shake your device. The result should look something like the image.

images/Sensors/first_acceleration_data.png

How cool is that? With these few lines of code, we managed to read sensor data from the device and draw it on the view. I’m sure you have lots of ideas for what you can do with these new skills. But before you explore those ideas, let’s finish our fun little app.

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

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