February 12, 2010

iPhone Tutorial: Red Square

This tutorial will lead you through creating the project that you will use as your assignment next week. By now you should be familiar with creating a basic application using XCode. So no dilly-dallying. Let’s get started.

Fire up XCode and select New Project from the main menu. We want to create a iPhone OS Application, specifically a View Based Application. You can call your application whatever you want and place this application where you like. This application template will create an project which has a boilerplate application delegate class and a boilerplate view controller class. We’re not going to mess with these at all.

We’re going to create a custom view, a subclass of UIView. Whenever you want to do custom drawing, in our case a fairly boring red square, you will generally need to subclass from UIView. In Cocoa the basic class for drawing is NSView. On the iPhone it is UIView. Even if you’re going to do OpenGL drawing, you will subclass from UIView. You can think about UIView as the basic class that allows for any kind of drawing to the screen. Every single visible component, from text field to button, is a subclass of UIView.

Let’s go ahead an create a custom UIView now, you should know how to add new files your project by now. You should create a UIView subclass, call it MyCustomView. This will add the header and source file to your project. Modify your header file to look like the following:

#import <UIKit/UIKit.h>
 
@interface MyCustomView : UIView
{
  CGFloat                    squareSize;
  CGFloat                    rotation;
  CGColorRef                 aColor;
  BOOL                       twoFingers;
 
  IBOutlet UILabel           *xField;
  IBOutlet UILabel           *yField;
  IBOutlet UILabel           *zField;
}
 
@end

This should look familiar from class. Here we’ve defined some variables for the square and some variables to hold the accelerometer data. IBOutlet you should be familiar with as well from the previous tutorials. If you don’t know what these mean now is some time to do some review.

Edit your source file to look like the following:

//
//  MyCustomView.m
//  RotateMe
//
//  Created by David Nolen on 2/16/09.
//  Copyright 2009 David Nolen. All rights reserved.
//
 
#import "MyCustomView.h"
 
#define kAccelerometerFrequency        10 //Hz
 
@implementation MyCustomView
 
- (id)initWithFrame:(CGRect)frame
{
  if (self = [super initWithFrame:frame])
  {
  }
  return self;
}
 
- (void) awakeFromNib
{
  // you have to initialize your view here since it's getting
  // instantiated by the nib
  squareSize = 100.0f;
  twoFingers = NO;
  rotation = 0.5f;
  // You have to explicity turn on multitouch for the view
  self.multipleTouchEnabled = YES;
 
  // configure for accelerometer
  [self configureAccelerometer];
}
 
-(void)configureAccelerometer
{
  UIAccelerometer*  theAccelerometer = [UIAccelerometer sharedAccelerometer];
 
  if(theAccelerometer)
  {
    theAccelerometer.updateInterval = 1 / kAccelerometerFrequency;
    theAccelerometer.delegate = self;
  }
  else
  {
    NSLog(@"Oops we're not running on the device!");
  }
}
 
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
  UIAccelerationValue x, y, z;
  x = acceleration.x;
  y = acceleration.y;
  z = acceleration.z;
 
  // Do something with the values.
  xField.text = [NSString stringWithFormat:@"%.5f", x];
  yField.text = [NSString stringWithFormat:@"%.5f", y];
  zField.text = [NSString stringWithFormat:@"%.5f", z];
}
 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  NSLog(@"touches began count %d, %@", [touches count], touches);
 
  if([touches count] &gt; 1)
  {
    twoFingers = YES;
  }
 
  // tell the view to redraw
  [self setNeedsDisplay];
}
 
- (void) touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
  NSLog(@"touches moved count %d, %@", [touches count], touches);
 
  // tell the view to redraw
  [self setNeedsDisplay];
}
 
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
  NSLog(@"touches moved count %d, %@", [touches count], touches);
 
  // reset the var
  twoFingers = NO;
 
  // tell the view to redraw
  [self setNeedsDisplay];
}
 
- (void) drawRect:(CGRect)rect
{
  NSLog(@"drawRect");
 
  CGFloat centerx = rect.size.width/2;
  CGFloat centery = rect.size.height/2;
  CGFloat half = squareSize/2;
  CGRect theRect = CGRectMake(-half, -half, squareSize, squareSize);
 
  // Grab the drawing context
  CGContextRef context = UIGraphicsGetCurrentContext();
 
  // like Processing pushMatrix
  CGContextSaveGState(context);
  CGContextTranslateCTM(context, centerx, centery);
 
  // Uncomment to see the rotated square
  //CGContextRotateCTM(context, rotation);
 
  // Set red stroke
  CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
 
  // Set different based on multitouch
  if(!twoFingers)
  {
    CGContextSetRGBFillColor(context, 0.0, 1.0, 0.0, 1.0);
  }
  else
  {
    CGContextSetRGBFillColor(context, 0.0, 1.0, 1.0, 1.0);
  }
 
  // Draw a rect with a red stroke
  CGContextFillRect(context, theRect);
  CGContextStrokeRect(context, theRect);
 
  // like Processing popMatrix
  CGContextRestoreGState(context);
}
 
- (void) dealloc
{
  [super dealloc];
}
 
@end

Wow that’s a doozy. All of this should make sense if you remember some of what I covered in class and you review the Event-Handling and Accelerometer sections from the iPhone Application Programming Guide. I’m only going to go over the relevant points. If there’s something you don’t understand you should run this project (after you’ve added the Interface Builder bits) and experiment using the debugger as well as well-placed NSLog statements.

- (void) awakeFromNib
{
  // you have to initialize your view here since it's getting
  // instantiated by the nib
  squareSize = 100.0f;
  twoFingers = NO;
  rotation = 0.5f;
  // You have to explicity turn on multitouch for the view
  self.multipleTouchEnabled = YES;
 
  // configure for accelerometer
  [self configureAccelerometer];
}

This class will be instantiated from the nib file so we must do all of our initialization in awakeFromNib. This is an important point to remember otherwise you’ll waste a lot of time trying to figure out why your class isn’t getting initialized properly. If you’re creating a class from code, normal initialization works, but if you instantiating a class from a nib file, you must initialize your instance in awakeFromNib.

Here we just setup the square size, a boolean member variable which tracks if we are in a two touch multitouch sequence. UIViews do not handle multitouch events by default. Here explicitly declare that this custom view wants to receive multitouch events. At the end we call our own method configureAccelerometer. This method is copied pretty much verbatim from Apple’s documentation so I won’t say much about it here.

The following method is accelerometer:didAccelerate:. This is the accelerometer delegate method you must implement if you want to receive accelerometer events. The signature must match exactly (the argument types and method return type- not then names). The only thing we do here is read in the accelerometer values and drop them into the textFields which we’ll define in Interface Builder.

Lets take a look at our first iPhone event handler:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
  NSLog(@"touches began count %d, %@", [touches count], touches);
 
  if([touches count] &gt; 1)
  {
    twoFingers = YES;
  }
 
  // tell the view to redraw
  [self setNeedsDisplay];
}

Again this is pretty self-explanatory and well covered in Apple’s docs. What might not be clear is that NSSet holds all of the touches in order! You do not have to identify which touch is which. Very handy. The other important part is the setNeedsDisplay call. UIViews do not automatically redraw their content. This is for performance reasons that are still relevant today. If something happens that requires you to redraw your view content, you must call setNeedsDisplay. Notice that we don’t call drawRect (the actually analog to Processing’s draw function) directly. This is important. As the programmer you have no idea when the most opportune time to update the screen will be. setNeedsDisplay tells the OS that when it has a chance it should redraw the contents of your view.

The other two touch event methods are more of the same, again, rereading Apple’s docs will clarify these. This takes us to drawRect, we will talk more about this in class. First thing to notice is that the drawing function is called drawRect, not drawCircle, or drawEllipse. All drawing surface on the iPhone are rectangular. Yes you can fake it with elliptical clipping regions and even crazier paths, but at the heart, all drawing on the iPhone is based on a rectangular region. Thus drawRect.

drawRect is pass in a C struct, CGRect that represents the rectangular region of the view. If you don’t know the fields of CGRect you should look this up in the documentation now. The first four lines of drawRect should be familiar to you if you’ve done some Processing work. The most interesting lines follow:

  // Grab the drawing context
  CGContextRef context = UIGraphicsGetCurrentContext();
 
  // like Processing pushMatrix
  CGContextSaveGState(context);
  CGContextTranslateCTM(context, centerx, centery);
 
  // Uncomment to see the rotated square
  //CGContextRotateCTM(context, rotation);
 
  // Set red stroke
  CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0);
 
  // Set different based on multitouch
  if(!twoFingers)
  {
    CGContextSetRGBFillColor(context, 0.0, 1.0, 0.0, 1.0);
  }
  else
  {
    CGContextSetRGBFillColor(context, 0.0, 1.0, 1.0, 1.0);
  }
 
  // Draw a rect with a red stroke
  CGContextFillRect(context, theRect);
  CGContextStrokeRect(context, theRect);
 
  // like Processing popMatrix
  CGContextRestoreGState(context);

In Processing the drawing context is implicit. If you’ve done any work with Eclipse and Processing then you know that you need to pass the graphics context around. Here we call UIGraphicsGetCurrentContext to get the drawable context. Whenever you call Quart2D APIs, you generally need to pass the context you want to draw to. Note that the stroke and the fill commands, though much more verbose, are philosophically identical to calls in Processing. The translate and rotate calls also should look familiar. The only two functions which looks strange are CGContextSaveGState and CGContextRestoreGState, and they are exactly like pushMatrix and popMatrix respectively. If you don’t know pushMatrix/popMatrix, again I suggest you set aside some time to understand what they accomplish in Processing as they effectively do the same thing in Cocoa programming.

Before moving onto Interface Builder you should create a repository on GitHub and commit this file (only MyCustomView.h and MyCustomView.m) in it’s current state to your repository. You should be able to figure this out. If you’re not sure, go over the Git tutorials.

So let’s switch gears to Interface Builder. Open the Resource group of your project and double click on the the view controller nib for your project. Interface Builder will launch and you should see view controller nib file. Double click on the view and make it look like the following:

events

Add three UILabels as shown above and give them the usual names. With the view selected in the nib window, open up the Inspector window and set the class of the the view to MyCustomView. You’re telling Interface Builder that the class of this view is the class you defined in XCode. Once the class has been set, right click on the view and you should get a HUD like display that has the outlets for the UILabels, connect these in the usual way.

You are no ready to run the program. Switch back to XCode and run your app using the Simulator. You should also try installing this application on the device. Notice that the phone easily multitasks between registering multitouch and accelerometer events.

You should now do the assignment for next week.

Leave a Reply