Tuesday, August 7, 2012

Çapless Paint for iPhone

In this post, we are going to implement a simple painting application for iOS. The name of the application will be Çapless Paint.

Before beginning, i want to show you a news from turkish media:http://www.ntvmsnbc.com/id/25371612 . Kılıcdaroglu (main opposition party leader) says "Çapless" to the minister of foreign affairs (Davutoğlu). Seems like we, çapless people, are getting larger in terms of number.

Anyway, lets talk about our new application: Çapless Paint. In this app, we will be able to select colors from a view in the screen, and paint anything we want in the selected color. 

Create a project in Xcode as you did in my previous blog and name it CaplessCoder. First of all, we will use a framework in this app: QuartzCore.framework. I will first show you how you can add frameworks to your project in Xcode.

Click on the project name from the file navigator bar in the left of Xcode. Select CaplessPaint under Targets section. When you scroll down in the new opened view, you will see "Linked Frameworks and Libraries". Press the add (+) button below and you will have a popup screen to select a framework. Type Quartz and double click on QuartzCore.framework from the results.

Figure 1

Now we are ready to code caplessly, Create an objective-c empty file named "Line", a subclass of NSObject. This Line file represents the lines that is drawn in the screen of iPhone. What information do we need in this class? Beginning and ending point of the line and the color of the line of course. Open your newly created class Line.h and change it with the following code:

//
//  Line.h
//  CaplessPaint
//
//  Created by Rıfat Ordulu on 7/24/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>
@interface Line : NSObject 

@property (nonatomic) CGPoint begin; 
@property (nonatomic) CGPoint end; 
@property (nonatomic, retain) UIColor *color;

@end


Then change your Line.m file as:

//
//  Line.m
//  CaplessPaint
//
//  Created by Rıfat Ordulu on 7/24/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import "Line.h" 

@implementation Line 

@synthesize begin, end, color;

- (id)init
{
    self = [super init];
    if (self) {
        [self setColor:[UIColor blackColor]];
    }
    return self;
}

@end


Let me explain this code a little bit. NSObject is a base class in objective-C and each class naturally extends NSObject. "@property" fields represents the properties of this Class and those properties are reached with a getter and setter if they are synthesized in the implementation file. 

- (id)init method is an overrode method, if you don't implement it you will get no error, but if you want to add some additional information when initialization of an object, you need to implement this method. self = [super init]; calls the super class of this "Line" object which is NSObject and sends it a message to initialize. Btw, don't confuse with the "self" word. it is exactly the same as "this" in Java. However, you can use "self" explicitly like a variable. you should also ask "What is that id in (id) init?" id is a memory pointer which shows the location of the object in the memory. You can use id to represent any object and you can also cast from id to anything you want, of course if you are sure that it can be casted. At the end, you see it returns itself? WTF??? it may be a little bit confusing but you will see why as you read the rest of this post.

Lets implement our ColorPicker class which represents a box in the view where the user will select a color. Create an objective-c empty file named "Line", a subclass of NSObject. Change the ColorPicker.h to:

//
//  ColorPicker.h
//  CaplessPaint
//
//  Created by Rıfat Ordulu on 8/3/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>

@protocol ColorPickerDelegate <NSObject>
@optional
-(void) aColorPickerIsSelected: (UIColor *) color;
@end

@interface ColorPicker : UIView

@property (nonatomic, assign) id <ColorPickerDelegate> delegate;

@end


Let's talk about what is going on with this "delegate" "protocol" and "optional" staff. In objective-C delegates are used to call some methods when a job is finished or an event occurred. It's like a listener in Java. @protocol ColorPickerDelegate <NSObject> means we're going to implement  a protocol for ColorPicker's objects. @optional means the responder of this delegate does not have to implement the delegate. the delegate method we defined is "aColorPickerIsSelected" with a parameter of UIColor class. Our only parameter @property (nonatomicassignid <ColorPickerDelegate> delegate; represents the responder of this delegate class. For example, if we will use the delegate of a ColorPicker from XViewController, the controller itself is stored as a property in ColorPicker as "delegate". As you can see it is a class "id". In the previous post, i've said id is a generic class of all classes in objective-C and it represents the memory address of an object. You can cast it to anything you want.

Change the implementation file "ColorPicker.m" to :

//
//  ColorPicker.m
//  TouchTracker
//
//  Created by Rıfat Ordulu on 8/3/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import "ColorPicker.h"

@implementation ColorPicker
@synthesize delegate;

- (void)touchesBegan:(NSSet *)touches
           withEvent:(UIEvent *)event
{
    if([delegate respondsToSelector:@selector(aColorPickerIsSelected:)])
        [delegate aColorPickerIsSelected:[self backgroundColor]];
}

@end


Here we are, what the hell does "- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event" means? The answer is very simple. Look at the header file "ColorPicker.h" you can see that we've changed the superclass of ColorPicker to UIView, and "touchesBegan" method is a generic method that can be used in subclasses of UIView. The method is triggered when a touch to the corresponding view is started. It has two parameters : "touches" and "event". Don't worry about them for now. if([delegate respondsToSelector:@selector(aColorPickerIsSelected:)]) means if the delegate responder "delegate" of this object responds to the selector (selector means a method btw) "aColorPickerIsSelected:" then, go. We'll soon see the responder of this delegate in our controller that we'll implement.  Then if it's true, it calls the method of the delegate responder with method aColorPickerIsSelected:[self backgroundColor] the parameter as you can see is the backgroundColor of the current picker view. 

I know you have a lot of questions, but in the end, all the missing pieces in your mind will fit. Now, where do we draw our capless drawings? Ofcourse in a view. We need to implement a UIView subclass where we can draw. Create an objective-C class named TouchDrawView a subclass of NSObject.

Change your TouchDrawView.h to :

//
//  TouchDrawView.h
//  TouchTracker
//
//  Created by Rıfat Ordulu on 7/24/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "Line.h"
@interface TouchDrawView : UIView {
    BOOL isCleared;
}
@property (nonatomic) Line * currentLine;
@property (nonatomic) NSMutableArray * linesCompleted;
@property (nonatomic) UIColor *drawColor;
- (void)clearAll;
@end

Okay, let me explain these properties and variables of this UIView subclass. isCleared is used to detect if the screen is cleared or not. We will implement a method such that if the user triple clicks the screen it will clear the screen and our isCleared variable will be true (YES in objective-C). currentLine is the line being drawn at the moment. linesCompleted is an array of lines that are drawn in the past. drawColor is the current selected drawing color for our CaplessPaint app. clearAll is just a method which you will understand why we use it in the implementation file.

Change the implementation file TouchDrawView.m to

//
//  TouchDrawView.m
//  TouchTracker
//
//  Created by Rıfat Ordulu on 7/24/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import "TouchDrawView.h"
#import "Line.h"
#import <QuartzCore/QuartzCore.h>

@implementation TouchDrawView

@synthesize linesCompleted;
@synthesize currentLine;
@synthesize drawColor;

- (id)initWithCoder:(NSCoder *)c
{
    self = [super initWithCoder:c];
    if (self) {
        linesCompleted = [[NSMutableArray alloc] init];
        [self setMultipleTouchEnabled:YES];
        
        drawColor = [UIColor blackColor];
        [self becomeFirstResponder];
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetLineWidth(context, 5.0);
    CGContextSetLineCap(context, kCGLineCapRound);
    [drawColor set];
    for (Line * line in linesCompleted) {
        [[line color] set];
        CGContextMoveToPoint(context, [line begin].x, [line begin].y);
        CGContextAddLineToPoint(context, [line end].x, [line end].y);
        CGContextStrokePath(context);
    } 
}


- (void)clearAll
{
    // Create a new layer that obscures the whole view
    CALayer *fadeLayer = [CALayer layer];
    [fadeLayer setBounds:[self bounds]];
    [fadeLayer setPosition:
     CGPointMake([self bounds].size.width / 2.0,
                 [self bounds].size.height / 2.0)];
    [fadeLayer setBackgroundColor:[[self backgroundColor] CGColor]];
    // Add this layer to the layer hierarchy on top of
    // the view's layer
    [[self layer] addSublayer:fadeLayer];
    // Create an animation that fades this layer in over 1 sec.
    CABasicAnimation *animation = [CABasicAnimation
                                   animationWithKeyPath:@"opacity"];
    [animation setFromValue:[NSNumber numberWithFloat:0]];
    [animation setToValue:[NSNumber numberWithFloat:1]];
    [animation setDuration:1];
    
    
    [CATransaction begin];
    // Set the completion block of this transaction
    // this method requires a block that returns no
    // value and accepts no argument: (void (^)(void))
    [CATransaction setCompletionBlock:^(void)
     {
         // When the animation completes, remove
         // the fadeLayer from the layer hierarchy
         [fadeLayer removeFromSuperlayer];
         // Also remove any completed or in process
         // lines
         [linesCompleted removeAllObjects];
         // Redisplay the view after lines are removed
         [self setNeedsDisplay];
     }];
    [fadeLayer addAnimation:animation forKey:@"Fade"];
    [CATransaction commit];
}

- (void)touchesBegan:(NSSet *)touches
           withEvent:(UIEvent *)event
{
    for (UITouch *t in touches) {
        // Is this a double tap?
        if ([t tapCount] > 2) {
            [self clearAll];
            isCleared = YES;
            return; }
        // Create a line for the value
        CGPoint loc = [t locationInView:self];
        Line *newLine = [[Line alloc] init];
        [newLine setBegin:loc];
        [newLine setEnd:loc];
        [newLine setColor:drawColor];
        currentLine = newLine;
    }
}

- (void)touchesMoved:(NSSet *)touches
withEvent:(UIEvent *)event
{
    if(!isCleared){
        for (UITouch *t in touches) {
            [currentLine setColor:drawColor];
            CGPoint loc = [t locationInView:self];
            [currentLine setEnd:loc];
            
            if (currentLine) {
                [linesCompleted addObject:currentLine];
            }
            Line *newLine = [[Line alloc] init];
            [newLine setBegin:loc];
            [newLine setEnd:loc];
            [newLine setColor:drawColor];
            currentLine = newLine;
        }
        [self setNeedsDisplay];
    }
}

- (void)endTouches:(NSSet *)touches
{
    if(!isCleared)
    {
        [self setNeedsDisplay];
    }
    else 
    {
        isCleared = NO;
    }
}

- (void)touchesEnded:(NSSet *)touches
           withEvent:(UIEvent *)event
{
    [self endTouches:touches];
}

- (void)touchesCancelled:(NSSet *)touches
               withEvent:(UIEvent *)event
{
    [self endTouches:touches];
}

- (BOOL)canBecomeFirstResponder
{
    return YES
}

- (void)didMoveToWindow
{
    [self becomeFirstResponder];
}


@end

Now this is a rough file to talk about. - (id)initWithCoder:(NSCoder *)c method is automatically called when created as we'll add this view to one of our xib files. First, we call the superclass's initWithCoder method and initialize with respect to the superclass. Then if the object is created successfully. we instantiate our variables and make the view firstResponder since we'll detect touch events and we want to trace the touches.


WHAT THE FUCK IS - (void)drawRect:(CGRect)rect ??? It is a method of UIView called every time the screen needs a redisplay or refresh. In this method you can draw to the screen whatever you like. Don't stuck with the complicated classes like CGContextSetLineWidth or CGContextMoveToPoint, just understand the idea that for every line (actually a line is a point in our app) we draw the representing painting in the screen.

As i've said before, - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event method is called when a touch begins. for (UITouch *t in touches) { loops for every touch of multiple touches. if the tap count is bigger than 2 we clear the drawing screen. Else, we create a line in the position of the touch and set the currentLine to our new line. 

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event method is called every time the touch is moved. if we are not clearing the screen we end the currentLine with the current position of the touch and create a new line with the beginning of the current position of the touch, then assign it to the current line. There you see that, every end of a line in touchesMoved event, is the starting of a new line.  Of course we add the finished line to the linesCompleted array. At the end we call the method [self setNeedsDisplay] which triggers the method: drawRect:.

If the touch is finished - (void)endTouches:(NSSet *)touches method is triggered by our - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event and - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event events as you can see the code. We do nothing there, since our drawing finished with the end of the touch. If we are in isCleared state we change it to NO, and if we are not in isCleared state we just refresh the screen by calling the method [self setNeedsDisplay];

- (BOOL)canBecomeFirstResponder
{
    return YES
}

- (void)didMoveToWindow
{
    [self becomeFirstResponder];
}


Those two methods are two little methods which save our code. We say, our view can become the first responder to trace the touch events. and of our view is moved to the current window we assign it to be the firstResponder. 

What is missing? a View file (.xib) of course!. Create and empty .xib file and name it TouchDrawViewController. and add an empty UIView component 

Add As much UIView component for the color picker boxes from the right-bottom menu as you like. I've added 16 color picker views as you can see in the figure 2:

Figure 2
You should be worried about how to assign the colors. After clicking the little square views click the  
 button from the right-top menu and change the background color as you wish.

Figure 3


You have to change the class of the each color picker view to "ColorPicker" as you've learned in the previous post. 

Moreover, we need to add a painting area to the screen. Add a UIView component to the empty area of the screen and assign its class to "TouchDrawView". Don't forget to click the files owner, and set the view outlet to the greater view that we've added to the screen first.

We're almost done. We've to implement our ViewController as you guess. Create an objective-C file named "TouchDrawViewController" a subclass of NSObject.

Change the file named TouchDrawViewController.h to:

//
//  TouchDrawViewController.h
//  TouchTracker
//
//  Created by Rıfat Ordulu on 7/24/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface TouchDrawViewController : UIViewController <ColorPickerDelegate>
{
    BOOL isCleared;
    BOOL isSelectedColor;
}
@end

You can see that we are implementing methods of ColorPickerDelegate to get notified when a delegate event specified in the ColorPicker class is occurred. Let's now set the view of this controller to our newly created TouchDrawViewController.xib and represent the components in our .xib file to this controller. Open up the TouchDrawViewController.xib file and by pressing control (kntrl) button drag and drop each color picker views and the painting area view to the header file ColorPickerViewController.h. 

Your TouchDrawViewController.h now should look like this:

//
//  TouchDrawViewController.h
//  TouchTracker
//
//  Created by Rıfat Ordulu on 7/24/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "TouchDrawView.h"
#import "ColorPicker.h"

@interface TouchDrawViewController : UIViewController <ColorPickerDelegate>
{
    BOOL isCleared;
    BOOL isSelectedColor;
}
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector1;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector2;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector3;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector4;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector5;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector6;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector7;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector8;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector9;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector10;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector11;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector12;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector13;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector14;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector15;
@property (unsafe_unretained, nonatomic) IBOutlet ColorPicker *selector16;

@property (retain, nonatomic) IBOutlet TouchDrawView *drawArea;
@end

Change your TouchDrawViewController.m file to:

//
//  TouchDrawViewController.m
//  TouchTracker
//
//  Created by Rıfat Ordulu on 7/24/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import "TouchDrawViewController.h"
#import "Line.h"
#import <QuartzCore/QuartzCore.h>
#import "TouchDrawView.h"

@implementation TouchDrawViewController
@synthesize selector1;
@synthesize selector2;
@synthesize selector3;
@synthesize selector4;
@synthesize selector5;
@synthesize selector6;
@synthesize selector7;
@synthesize selector8;
@synthesize selector9;
@synthesize selector10;
@synthesize selector11;
@synthesize selector12;
@synthesize selector13;
@synthesize selector14;
@synthesize selector15;
@synthesize selector16;
@synthesize drawArea;

- (id)init {
    self = [super initWithNibName:@"TouchDrawViewController"
                           bundle:nil];
    isSelectedColor = NO;
    isCleared = NO;
    
    return self;
}
// This method gets called automatically when the view is created
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    [selector1 setDelegate:self];
    [selector2 setDelegate:self];
    [selector3 setDelegate:self];
    [selector4 setDelegate:self];
    [selector5 setDelegate:self];
    [selector6 setDelegate:self];
    [selector7 setDelegate:self];
    [selector8 setDelegate:self];
    [selector9 setDelegate:self];
    [selector10 setDelegate:self];
    [selector11 setDelegate:self];
    [selector12 setDelegate:self];
    [selector13 setDelegate:self];
    [selector14 setDelegate:self];
    [selector15 setDelegate:self];
    [selector16 setDelegate:self];
    
    NSLog(@"Loaded the view for HypnosisViewController");
    // Set the background color of the view so we can see it
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
}

-(void) aColorPickerIsSelected:(UIColor *)color
{
    [drawArea setDrawColor:color];
}

@end

In viewDidLoad method you see that all the selectors' (color pickers) delegate are set to self  [selector1 setDelegate:self];  . Which means that the delegate of each color picker view is this controller: TouchDrawViewController. Therefore we let all the ColorPicker view objects to know that their delegate is TouchDrawViewController and they can send message to it. 

You see that we've implemented the method that we specified in our ColorPicker class : -(void) aColorPickerIsSelected:(UIColor *)color . we get the color (this color is sent from the ColorPicker class) and set it to drawArea's drawColor. 

Last thing to do is to make this controller the main controller. Go to the CaplessPaintAppDelegate.h and change it to:

//
//  CaplessPaintAppDelegate.h
//  CaplessPaint
//
//  Created by Rıfat Ordulu on 7/24/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface CaplessPaintAppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end


And change your CaplessPaintAppDelegate.m file to:

//
//  CaplessPaintAppDelegate.m
//  CaplessPaint
//
//  Created by Rıfat Ordulu on 7/24/12.
//  Copyright (c) 2012 __MyCompanyName__. All rights reserved.
//

#import "CaplessPaintAppDelegate.h"
#import "TouchDrawView.h"
#import "TouchDrawViewController.h"

@implementation CaplessPaintAppDelegate

@synthesize window = _window;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    
    TouchDrawViewController *ct = [[TouchDrawViewController alloc] init];
    
    // Set tabBarController as rootViewController of window
    [[self window] setRootViewController:ct];
    
    return YES;
}
@end


That's it, just run the app in the simulator and see the results, get happy. In this app, there is poor memory management, and i'll tell the problem and solve it in the upcoming post. Thank you all who've read the blog up to here. 

Here is the some snapshots of the app:






8 comments:

  1. i havent seen anyone as capless as this guy in my entire life! Congs bro.

    ReplyDelete
  2. hi,Whener i click on other colors it says bad access..pls help...

    ReplyDelete
  3. Where does this "Bad Access" error come from? in which class?

    ReplyDelete
  4. Thank you very much, it very clear and good guide! I hope you'll public more example

    ReplyDelete
  5. Can i get this sample example using c#..?

    ReplyDelete
  6. thanks for your example, it simple to understand, but this solution is not good, it will get slower and slower.

    ReplyDelete