iOS – Accelerometer App

A few weeks ago, I wrote about how to access the iPhone accelerometer using HTML5 and the PhoneGap API. In this post, I return to this topic and discuss a native Accelerometer app. Here is a screenshot of version 1.0:Screen Shot 2014-05-09 at 4.34.39 AM

In the above picture, I’ve kept the iPhone on a table, with the x and y axes in the plane of the table. The z-axis points up towards you, normal to the iPhone screen. The acceleration due to gravity (in our non-inertial frame of reference on the earths surface) is negative one (in units of g = 9.8 [m/s2]), which means it is in the direction of the negative z-axis. The numbers show the acceleration magnitude and the {x,y,z} components, with the minimum and maximum values. Pressing the reset button erases the minimum and maximum values.

The polar grid is for displaying the instantaneous acceleration in the x-y plane. Because the acceleration is along z in this picture, the yellow ball is at the origin (zero x and y components). The circles correspond to different g values. If we place the iPhone on its edge in a vertical position on the table, we get the following result:

Screen Shot 2014-05-09 at 4.47.58 AM

As you expected, the x-component is now 1.0 and the y and z components are zero. You can play with this app by downloading the source code from

https://github.com/jabhiji/ios-accelerometer.git

If you’ve been following my journey in iOS land, there were primarily two new things I learned while making this app:

  • Using the CoreMotion Framework – this is where all the “motion sensing” classes live. You need to add this framework to your project and instantiate a CMMotionManager object to talk to the accelerometer.
#import <UIKit/UIKit.h>
#import <CoreMotion/CoreMotion.h>
#import "Model.h"
#import "Circles.h"

@interface ViewController : UIViewController

// labels
@property (strong, nonatomic) IBOutlet UILabel *minMag;
@property (strong, nonatomic) IBOutlet UILabel *magnitude;
@property (strong, nonatomic) IBOutlet UILabel *maxMag;
@property (strong, nonatomic) IBOutlet UILabel *accX;
@property (strong, nonatomic) IBOutlet UILabel *accY;
@property (strong, nonatomic) IBOutlet UILabel *accZ;
@property (strong, nonatomic) IBOutlet UILabel *minX;
@property (strong, nonatomic) IBOutlet UILabel *minY;
@property (strong, nonatomic) IBOutlet UILabel *minZ;
@property (strong, nonatomic) IBOutlet UILabel *maxX;
@property (strong, nonatomic) IBOutlet UILabel *maxY;
@property (strong, nonatomic) IBOutlet UILabel *maxZ;

// motion manager
@property (strong, nonatomic) CMMotionManager *motionManager;

// rolling ball model
@property (strong, nonatomic) Model *model;

// the view
@property (strong, nonatomic) IBOutlet Circles *myView;

// the ball images
@property (nonatomic, strong) UIImage* yellowball;
@property (nonatomic, strong) UIImageView* ballView;
- (IBAction)resetValues:(id)sender;

@end

The implementation file shows how to initialize this new object and use it to talk to the accelerometer:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

@synthesize minMag, magnitude, maxMag;
@synthesize minX, minY, minZ;
@synthesize accX, accY, accZ;
@synthesize maxX, maxY, maxZ;
@synthesize model;
@synthesize myView;
@synthesize motionManager;
@synthesize yellowball;
@synthesize ballView;

- (void)viewDidLoad
{
    [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
    
    // ball image
    yellowball = [UIImage imageNamed:@"yellowBall.png"];
    
    // draw custom drawing inside the view (as background)
    [myView setNeedsDisplay];
    
    // crop anything that is outside the view
    myView.clipsToBounds = YES;
    
    // initialize model
    model = [[Model alloc] init];
    model.width  = self.myView.frame.size.width;
    model.height = self.myView.frame.size.height;
    [model setInitialBallPosition];
    
    // initialize UIIMageView object
    ballView = [[UIImageView alloc] init];
    ballView.image = yellowball;
    
    // initialize motion manager
    motionManager = [[CMMotionManager alloc] init];
    motionManager.accelerometerUpdateInterval = 1.0/60.0;

    if ([motionManager isAccelerometerAvailable]) {
        
        [self startGameLoop];
        
    } else {
        
        NSLog(@"No accelerometer! You may be running on the iOS simulator...");
    }

}

// get acceleration data and animate ball motion based on current acceleration
- (void) startGameLoop
{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    [motionManager startAccelerometerUpdatesToQueue:queue withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
        dispatch_async(dispatch_get_main_queue(), ^{
          
            // acceleration components (X, Y and Z)
            model.accelerationX = accelerometerData.acceleration.x;
            model.accelerationY = accelerometerData.acceleration.y;
            model.accelerationZ = accelerometerData.acceleration.z;
.
.
.
.

@end
  • Using the CoreGraphics API for custom drawing inside a UI View, starting with getting the current graphics context and learning the difference between points and pixels. This stuff is found in Circles.m, the implementation file for the Circles class.
#import "Circles.h"

@implementation Circles

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}

// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    [self drawCircles];
}

// polar grid on which the instantaneous acceleration is displayed in real time
- (void) drawCircles
{
    // get the current context
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // context size in pixels
    size_t WIDTH = CGBitmapContextGetWidth(context);
    size_t HEIGHT = CGBitmapContextGetHeight(context);
    
    // for retina display, 1 point = 2 pixels
    // context size in screen points
    float width = WIDTH/2.0;
    float height = HEIGHT/2.0;
    
    // center coordinates
    float xCen = width/2.0;
    float yCen = height/2.0;
    float maxR = width/2.0;     // WIDTH = HEIGHT in this app
    float ONEGEE = 0.666*maxR;
    
    // big circle to fill corners
    CGContextBeginPath(context);
    CGContextAddArc(context, xCen, yCen, 3*ONEGEE, 0, 2*M_PI, YES);
    [[UIColor blueColor] setFill];
    [[UIColor blackColor] setStroke];
    CGContextDrawPath(context, kCGPathFillStroke);
    
    // outermost red circle (2g)
    CGContextBeginPath(context);
    CGContextAddArc(context, xCen, yCen, 2*ONEGEE, 0, 2*M_PI, YES);
    [[UIColor blueColor] setFill];
    [[UIColor blackColor] setStroke];
    CGContextDrawPath(context, kCGPathFillStroke);
    
    // blue circle (1g)
    CGContextBeginPath(context);
    CGContextAddArc(context, xCen, yCen, ONEGEE, 0, 2*M_PI, YES);
    [[UIColor blueColor] setFill];
    [[UIColor blackColor] setStroke];
    CGContextDrawPath(context, kCGPathFillStroke);
    
    // unfilled circles (0.5g)
    CGContextBeginPath(context);
    CGContextAddArc(context, xCen, yCen, 0.5*ONEGEE, 0, 2*M_PI, YES);
    [[UIColor blackColor] setStroke];
    CGContextDrawPath(context, kCGPathStroke);
    
    // unfilled circles (1.5g)
    CGContextBeginPath(context);
    CGContextAddArc(context, xCen, yCen, 1.5*ONEGEE, 0, 2*M_PI, YES);
    [[UIColor blackColor] setStroke];
    CGContextDrawPath(context, kCGPathStroke);
    
    // horizontal centerline
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, 0, height/2);
    CGContextAddLineToPoint(context, width, height/2);
    [[UIColor blackColor] setStroke];
    CGContextDrawPath(context, kCGPathStroke);
    
    // vertical centerline
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, width/2, 0);
    CGContextAddLineToPoint(context, width/2, height);
    [[UIColor blackColor] setStroke];
    CGContextDrawPath(context, kCGPathStroke);
    
    // diagonal 1
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, 0, 0);
    CGContextAddLineToPoint(context, width, height);
    [[UIColor blackColor] setStroke];
    CGContextDrawPath(context, kCGPathStroke);

    // diagonal 2
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, width, 0);
    CGContextAddLineToPoint(context, 0, height);
    [[UIColor blackColor] setStroke];
    CGContextDrawPath(context, kCGPathStroke);
}

@end

—-

Once your device has this app, there are some interesting experiments you can do to test how many g’s your body experiences while walking, running, walking up and down the stairs and so on and so forth. Here is a quick summary of the acceleration magnitude range in some random experiments:

  • Standing still – 1g
  • Normal walking – 0.7g to 1.8g
  • Walking up the stairs – 0.4g to 2.0g
  • Going downstairs – 0.1g to 3.3g
  • Jumping up and down – 0g to 2.5g
  • Vigorously shaking the phone – 0g to 3.5g

Finally, if you throw your iPhone in the air, it will temporarily experience “weightless” conditions (just like it does when you jump up and down) and the app will record the minimum acceleration magnitude to be zero. But the impulse when you throw it and catch it again could be up to 3.5g. The above set of results come with some caveats that you should keep in mind: (1) The data is obtained at 60 Hz. Thus, impulses imparted in a time interval less than 1/60th of a second may not be recorded accurately. (2) I was not able to get a reading more than 3.5g for the maximum acceleration. This might be the maximum limit of the hardware.

In summary, we learned how to write a native iOS app that queries the built-in accelerometer and displays the values on screen in real-time. In the next version of this app, I plan to add in another screen where we can plot the different accelerations as a single graph or multiple graphs, with time on the x-axis.

Leave a Reply

Your email address will not be published. Required fields are marked *