iOS 5 View Programming & Drawing

Create a Custom View for iOS

Create a new project containing a custom view - a subclass of UIView

  • Create a new Project Circle: File -> New -> New Project ...
  • Create a custom iOS View
    • Select File -> New -> New File ...
    • Select Objective-C class
    • Select Subclass of UIView
    • Save as CircleView

Create an iOS 5 View with Xcode 4.2

Creating a custom view

  • That draw a circle and
  • A slider controlling the radius of the circle

Steps:

  • Select CircleViewController_iPhone.xib
  • Open the Utility Panel and select the Object Library in the bottom panel
  • Create a custom view to draw a circle
    • Drap a View into the editor
    • Select the Identity Inspector on the top panel of the Utility panel
    • Select CircleView as the Class
  • Add a Slider object to the editor with the Object Library
  • Create and connect outlets & actions in Xcode Interface Builder
    • Show the Assistant Editor
    • Declare an IBOutlet by control click on the View and drap it between the code @interface and @end in CircleViewController.h
      • Name it circle
    • Control click on the Slider and drap in to CircleViewController.h
      • Name it slider
    • Control click on the Slider and drap in to CircleViewController.h
      • Select Action in Connection
      • Name it as slide
      • Select UISlider for Type

Interface Builder generates the following code:

CircleViewController.h
#import <UIKit/UIKit.h>
#import "CiricleView.h"

@interface CircleViewController : UIViewController {
    CiricleView *circle;
    UISlider *slider;
}

@property (strong, nonatomic) IBOutlet CiricleView *circle;
@property (strong, nonatomic) IBOutlet UISlider *slider;

- (IBAction)slide:(id)sender;

@end

Implement an iOS View Class

The view header file

CircleView.h
#import <UIKit/UIKit.h>

@interface CiricleView : UIView

// The scaling factor for the circle (Value controlled by a slider)
@property double scale;

@end

iOS Custom View implementation class

CircleView.m
#import "CiricleView.h"

@implementation CiricleView

@synthesize scale;

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

- (void)drawCircle:(CGPoint)p withRadius:(CGFloat)radius inContext:(CGContextRef)context
{
	UIGraphicsPushContext(context);
	CGContextBeginPath(context);
	CGContextAddArc(context, p.x, p.y, radius, 0, 2*M_PI, YES);
	CGContextStrokePath(context);
	UIGraphicsPopContext();
}

- (void)drawRect:(CGRect)rect
{
	CGPoint point;
	point.x = self.bounds.origin.x + self.bounds.size.width/2;
        point.y = self.bounds.origin.y + self.bounds.size.height/2;

	CGFloat size = self.bounds.size.width / 2;
	if (self.bounds.size.height < self.bounds.size.width) size = self.bounds.size.height / 2;
	size *= 0.90;
        if (self.scale > 0) size *= self.scale;

	CGContextRef context = UIGraphicsGetCurrentContext();

	CGContextSetLineWidth(context, 5.0);
	[[UIColor redColor] setStroke];

	[self drawCircle:point withRadius:size inContext:context];
}

@end
  • When iOS needs to redraw a view, iOS calls the view's drawRect to redraw its view content
  • drawRect should never be called directly
    - (void)drawRect:(CGRect)rect
    {
      ...
    }
    
  • Calculate the center and the radius of the circle
    	CGPoint point;
    	point.x = self.bounds.origin.x + self.bounds.size.width/2;
            point.y = self.bounds.origin.y + self.bounds.size.height/2;
    
    	CGFloat size = self.bounds.size.width / 2;
    	if (self.bounds.size.height < self.bounds.size.width) size = self.bounds.size.height / 2;
    	size *= 0.90;
            if (self.scale > 0) size *= self.scale;
    
  • Get the current graphics context
  • Change the stroke color and line width
  • Call the drawCircle message in our view's code to draw the circle
    	CGContextRef context = UIGraphicsGetCurrentContext();
    
    	CGContextSetLineWidth(context, 5.0);
    	[[UIColor redColor] setStroke];
    	[self drawCircle:point withRadius:size inContext:context];
    
  • With UIGraphicsPushContext, any UI configuration changes before the push (like the original fill color) will be restored after UIGraphicsPopContext
  • Allow us to restore the graphic configuration state before we made custom changes
    - (void)drawCircle:(CGPoint)p withRadius:(CGFloat)radius inContext:(CGContextRef)context
    {
    	UIGraphicsPushContext(context);
            ...
    	UIGraphicsPopContext();
    }
    
  • Create a path and stroke it with the stroke color
    	CGContextBeginPath(context);
    	CGContextAddArc(context, p.x, p.y, radius, 0, 2*M_PI, YES);
    	CGContextStrokePath(context);
    

Redraw View when Device is Rotated

When a device is rotate, iOS may re-scale the current view automatically without asking the View to draw it again

  • This may turn the circle we draw previously into a eclipse
  • To request a redraw automatically
    - (void)setup
    {
       self.contentMode = UIViewContentModeRedraw;
    }
    
    - (id)initWithFrame:(CGRect)frame {
        if ((self = [super initWithFrame:frame])) {
            // Initialization code
    	[self setup];
        }
        return self;
    }
    
    - (void)awakeFromNib
    {
        [self setup];
    }
    

Implement a slider action to control the circle's radius

Program the slider to control the radius of the drawing circle in the custom view

  • Get the slider value inside the slider action handler
  • Set the scaling factor for the custom view's circle
    CircleViewController.m
    - (IBAction)slide:(id)sender {
        UISlider *s = sender;
        self.circle.scale = s.value;
        [self.circle setNeedsDisplay];
    }
    

An application never call the view directly to redraw a view

  • instead it requests iOS to redraw the view with the message setNeedsDisplay
        [self.circle setNeedsDisplay];
    

Basic iOS Drawing Data Structures & Concepts

Co-ordinate structure

  • top left is (0,0)
  • horizontal is the x-axis
CGFloat   Floating Point Value
CGPoint
CGPoint point = CGPointMake(30.1, 10.5);
point.x += 10; 
A Point Object
CGSize
CGSize size = CGSizeMake(100.0, 200.0);
size.height += 10;
A Size object
CGRect
CGRect rect = CGRectMake(45.0, 75.5, 300, 500);
fect.size.height += 15;
rect.origin.x += 20; 
A Rectangular object

A iOS view Boundary

	point.x = self.bounds.origin.x + self.bounds.size.width/2;
        point.y = self.bounds.origin.y + self.bounds.size.height/2;

View Boundary

Object describing a view boundary self.bounds = ((0,0),(100,150))
The enclosing rectangular of the view relative to the root view self.frame
The center of the view self.center
self.bounds.origin.x
self.bounds.size.width

iOS Path API

CGContextBeginPath(context);
CGContextMoveToPoint(context, 10, 10);
CGContextAddLineToPoint(context, 10, 30);
CGContextAddLineToPoint(context, 30, 30);
CGContextClosePath(context);
CGContextStrokePath(context);
  • Move to point (10,10)
  • Draw path from (10,10) to (10, 30) to (30,30)
  • Close the path back to (10,10)

Changing Graphics State

Setting the Fill and Stroke color

[[UIColor redColor] setFill];
[[UIColor blueColor] setStroke];
CGContextDrawPath(context,kCGPathFillStroke);

Customize a color and set it to both fill and stroke color

UIColor *color = [[UIColor alloc] initWithRed:(CGFloat)red
                                          blue:(CGFloat)blue
                                         green:(CGFloat)green
                                         alpha:(CGFloat)alpha;
[color set];
  • Alpha control the transparency of the color when drawing
  • @property BOOL opaque of the view must set to NO for transparency

Setting a fill pattern & line width

CGContextSetFillPattern(context, (CGPatternRef)pattern, (CGFloat[])comp)
CGContextSetLineWidth(context,1.0);

Drawing

Drawing Text Manually in iOS

UIFont *font = [UIFont systemFontOfSize:8.0];

NSString *text = @"testing";
[text drawAtPoint:(CGPoint)point withFont:font];
NSString *text = @"My Title";

UIFont *font = [UIFont systemFontOfSize:HASH_MARK_FONT_SIZE];

CGRect rect;
rect.size = [text sizeWithFont:font];
rect.origin.x = location.x - rect.size.width / 2;
rect.origin.y = location.y - rect.size.height / 2;

[text drawInRect:rect withFont:font];

Draw Image Manually in iOS

UIImage *image = [UIImage imageNamed:@"icon.jpg"];

[image drawAtPoint:(CGPoint)point];
[image drawInRect:(CGRect)r];

Set a Background Image in a View

Implement a setter method to set a background image

  • Create a UIImageView (if nil)
  • Initialize the view with the full size view boundary
  • Set the UIImageView to a subview of the current controller's view
  • Subviews with smaller index value show under views with higher index value
  • Set the image of the UIImageView with the user input image
    - (UIImageView *)bgImageView
    {
      id subview = [self.view.subviews objectAtIndex:0];
      if ([subview isKindOfClass:[UIImageView class]]) {
        return (UIImageView *) subview;
      } else {
        return nil;
      }
    }
    
    - (void)setBgImage:(UIImage *)image
    {
      UIImageView *bg = self.bgImageView;
      if (!bg) {
        bg = [[UIImageView alloc] initWithFrame:self.view.bounds];
        [self.view insertSubview:bg atIndex:0];
      }
      bg.image = image;
    }
    

Draw a curve in iOS

CGContextAddCurveToPoint(context, 60.0, 170, 50.0, 160.0, 90.0, 90.0);

iOS View Object Programming

Add a iOS View button programmatically

CGRect rect = CGRectMake(10, 10, 50, 50);
UIButton *button = [[UIButton alloc] initWithFrame:rect];
button.titleLabel.text = @"My Button";
[window addSubview:button];

Toggle between Multiple iOS Views

Toggle between 2 iOS Views

  • Initialize 2 subviews that can toggle between each other
  • Add both as subviews
  • Create a toggle button on the navigation menu
  • Use the hidden property to tell iOS to show the view or not
    MyViewController.m
    - (MyView1 *)view1
    {
      if (!view1)
        view1 = [[MyView1 alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
      return view1;
    }
    
    - (MyView2 *)view2
    {
      if (!view1)
        view2 = [[MyView2 alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
      return view2;
    }
    
    - (void)viewDidLoad
    {
       [super viewDidLoad];
    
       self.view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
    
       self.view1.frame = self.view.bounds;
       [self.view addSubview:self.view1];
    
       self.view2.frame = self.view.bounds;
       [self.view addSubview:self.view2];
    
       self.view2.hidden = YES;
    
       self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"View2"
               style:UIBarButtonItemStyleBordered target:self action:@selector(toggle)];
    }
    
  • We assume this view controller is already pushed to a navigation view controller so we can use the navigationItem

Toggle between 2 subviews

- (void)toggle
{
  if (self.view2.isHidden) {
    self.view2.hidden = NO;
    self.view1.hidden = YES;
    self.navigationItem.rightBarButtonItem.title = @"View1";
  } else {
    self.view1.hidden = YES;
    self.view2.hidden = NO;
    self.navigationItem.rightBarButtonItem.title = @"View2";
  }
}