Mega-Gold Challenge


#1

This one was really hard for me. Spent a few hours on it, I think. Eventually I got it working :slight_smile:

In Line.h

In Line.m

From now on I’ll post my files in their entirety.

TouchDrawView.h

[code]#import <Foundation/Foundation.h>
#import “CPViewController.h”

@class Line;

@interface TouchDrawView : UIView <UIGestureRecognizerDelegate, CPViewControllerDelegate> {
NSMutableDictionary *linesInProcess;
NSMutableArray *completeLines;
UIPanGestureRecognizer *moveRecognizer;
}

@property (nonatomic, weak) Line *selectedLine;
@property (nonatomic, strong) UIColor *lineColor;

-(void)clearAll;
-(void)endTouches:(NSSet *)touches;
-(Line *)lineAtPoint:(CGPoint)p;

@end[/code]

TouchDrawView.m

[code]#import “TouchDrawView.h”
#import “Line.h”

@implementation TouchDrawView

@synthesize selectedLine, lineColor;

-(id)initWithFrame:(CGRect)r {
self = [super initWithFrame:r];

if(self) {
	linesInProcess = [[NSMutableDictionary alloc] init];
	
	// Don't let the autocomplete fool you on the next line,
	// make sure you are instantiating an NSMutableArray
	// and not an NSMutableDictionary
	completeLines = [[NSMutableArray alloc] init];
	
	[self setBackgroundColor:[UIColor whiteColor]];
	[self setMultipleTouchEnabled:YES];
	
	UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
	[self addGestureRecognizer:tapRecognizer];
	
	UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
	[doubleTapRecognizer setNumberOfTapsRequired:2];
	[self addGestureRecognizer:doubleTapRecognizer];
	
	UILongPressGestureRecognizer *pressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
	[self addGestureRecognizer:pressRecognizer];
	
	UISwipeGestureRecognizer *swipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(threeSwipeUp:)];
	[swipeRecognizer setDirection:UISwipeGestureRecognizerDirectionUp];
	[swipeRecognizer setNumberOfTouchesRequired:3];
	[self addGestureRecognizer:swipeRecognizer];
	
	moveRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(moveLine:)];
	[moveRecognizer setDelegate:self];
	[moveRecognizer setCancelsTouchesInView:NO];
	[self addGestureRecognizer:moveRecognizer];
}

return self;

}

-(void)threeSwipeUp:(UIGestureRecognizer *)gr {
[linesInProcess removeAllObjects];

CPViewController *cpvc = [[CPViewController alloc] initWithNibName:@"CPViewController" bundle:nil];
[cpvc setDelegate:self];

NSInteger selectedSegment;

if(!lineColor) {
	selectedSegment = 0;
}
else {
	if(lineColor == [UIColor redColor]) selectedSegment = 0;
	if(lineColor == [UIColor greenColor]) selectedSegment = 1;
	if(lineColor == [UIColor blueColor]) selectedSegment = 2;
}
[cpvc setSelectedSegment:selectedSegment];
[[[self window] rootViewController] presentModalViewController:cpvc
													   animated:YES];

}

-(void)setLineColor:(CPViewController *)controller color:(UIColor *)clr {
lineColor = clr;
[self reloadInputViews];
}

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
if(gestureRecognizer == moveRecognizer) {
return YES;
}
else {
return NO;
}
}

-(void)moveLine:(UIPanGestureRecognizer *)gr {
// If we haven’t selected a line, we don’t do anything here
if(![self selectedLine]) {
return;
}

// When the pan recognizer changes its position...
if([gr state] == UIGestureRecognizerStateChanged) {
	// How far was the pan moved?
	CGPoint translation = [gr translationInView:self];
	
	// Add the translation to the current begin and end points of the line
	CGPoint begin = [[self selectedLine] begin];
	CGPoint end = [[self selectedLine] end];
	begin.x += translation.x;
	begin.y += translation.y;
	end.x += translation.x;
	end.y += translation.y;
	
	// Set the new beginning and end points of the line
	[[self selectedLine] setBegin:begin];
	[[self selectedLine] setEnd:end];
	
	// Redraw the screen
	[self setNeedsDisplay];
	
	[gr setTranslation:CGPointZero inView:self];
}

}

-(void)longPress:(UIGestureRecognizer *)gr {
if([gr state] == UIGestureRecognizerStateBegan) {
CGPoint point = [gr locationInView:self];
[self setSelectedLine:[self lineAtPoint:point]];

	if([self selectedLine]) {
		[linesInProcess removeAllObjects];
	}
}
else if([gr state] == UIGestureRecognizerStateEnded) {
	[self setSelectedLine:nil];
}

[self setNeedsDisplay];

}

-(Line *)lineAtPoint:(CGPoint)p {
// Find a line close to p
for(Line *l in completeLines) {
CGPoint start = [l begin];
CGPoint end = [l end];

	// Check a few points on the line
	for(float t = 0.0; t <= 1.0; t += 0.05) {
		float x = start.x + t * (end.x - start.x);
		float y = start.y + t * (end.y - start.y);
		
		// If the tapped point is within 20 points, let's return this line
		if(hypot(x - p.x, y - p.y) < 20.0) {
			return l;
		}
	}
}

// If nothing is close enough to the tapped point, then we didn't select a line
return nil;

}

-(BOOL)canBecomeFirstResponder {
return YES;
}

-(void)deleteLine:(id)sender {
// Remove the selected line from the list of completedLines
[completeLines removeObject:[self selectedLine]];

// Redraw everything
[self setNeedsDisplay];

}

-(void)doubleTap:(UIGestureRecognizer *)gr {
[self clearAll];
[self setNeedsDisplay];
}

-(void)tap:(UIGestureRecognizer *)gr {
CGPoint point = [gr locationInView:self];
[self setSelectedLine:[self lineAtPoint:point]];

[linesInProcess removeAllObjects];

if([self selectedLine]) {
	[self becomeFirstResponder];
	// Grab the menu controller
	UIMenuController *menu = [UIMenuController sharedMenuController];
	
	// Create a new "Delete" UIMenuItem
	UIMenuItem *deleteItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(deleteLine:)];
	[menu setMenuItems:[NSArray arrayWithObject:deleteItem]];
	
	// Tell the menu where it should come from and show it
	[menu setTargetRect:CGRectMake(point.x, point.y, 2, 2) inView:self];
	[menu setMenuVisible:YES animated:YES];
}
else {
	// Hide the menu if no line is selected
	[[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
}

[self setNeedsDisplay];

}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if([self selectedLine]) {
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
[self setSelectedLine:nil];
}

for(UITouch *t in touches) {
	// Use the touch object (packed in an NSValue) as the key
	NSValue *key = [NSValue valueWithNonretainedObject:t];

	// Create a line for the value
	CGPoint loc = [t locationInView:self];
	Line *newLine = [[Line alloc] init];
	[newLine setBegin:loc];
	[newLine setEnd:loc];
	[newLine setThickness:1.0];
	[newLine setLineColor:lineColor];
	// Put pair in dictionary
	[linesInProcess setObject:newLine forKey:key];
}

}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// Update linesInProgress with moved touched
for(UITouch *t in touches) {
NSValue *key = [NSValue valueWithNonretainedObject:t];

	// Find the line for this touch
	Line *line = [linesInProcess objectForKey:key];
	
	// Update the line
	CGPoint loc = [t locationInView:self];
	[line setEnd:loc];
	
	// Determine line thickness based on drawing velocity
	CGPoint drawVelocity = [moveRecognizer velocityInView:self];
	drawVelocity.x = fabsf(drawVelocity.x);
	drawVelocity.y = fabsf(drawVelocity.y);
	
	CGFloat drawSpeed;
	if(drawVelocity.x > drawVelocity.y) {
		drawSpeed = drawVelocity.x - drawVelocity.y;
	}
	else {
		drawSpeed = drawVelocity.y - drawVelocity.x;
	}
	
	[line setThickness:drawSpeed / 10];
}

// Redraw
[self setNeedsDisplay];

}

-(UIColor *)lineColor {
if(!lineColor) {
return [UIColor redColor];
}
else {
return lineColor;
}
}

-(void)endTouches:(NSSet *)touches {
// Remove ending touches from dictionary
for(UITouch *t in touches) {
NSValue *key = [NSValue valueWithNonretainedObject:t];
Line *line = [linesInProcess objectForKey:key];

	// If this is a double tap, 'line' will be nil
	// so make sure not to add it to the array
	if(line) {
		[completeLines addObject:line];
		[linesInProcess removeObjectForKey:key];
	}
}

// Redraw
[self setNeedsDisplay];

}

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

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

-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineCap(context, kCGLineCapRound);
[[UIColor redColor] set];

for(Line *line in completeLines) {
	if(![line lineColor]) {
		[line setLineColor:lineColor];
	}
	
	[[line lineColor] set];
	CGContextSetLineWidth(context, [line thickness]);
	CGContextMoveToPoint(context, [line begin].x, [line begin].y);
	CGContextAddLineToPoint(context, [line end].x, [line end].y);
	CGContextStrokePath(context);
}

// Draw lines in process in red (don't copy and paste the previous loop;
// this one is way different
for(NSValue *v in linesInProcess) {
	Line *line = [linesInProcess objectForKey:v];
	
	if(![line lineColor]) {
		[line setLineColor:lineColor];
	}
	
	[[line lineColor] set];
	CGContextSetLineWidth(context, [line thickness]);
	CGContextMoveToPoint(context, [line begin].x, [line begin].y);
	CGContextAddLineToPoint(context, [line end].x, [line end].y);
	CGContextStrokePath(context);
}

// If there is a selected line, draw it
if([self selectedLine]) {
	[[UIColor greenColor] set];
	CGContextSetLineWidth(context, [[self selectedLine] thickness]);
	CGContextMoveToPoint(context, [[self selectedLine] begin].x, [[self selectedLine] begin].y);
	CGContextAddLineToPoint(context, [[self selectedLine] end].x, [[self selectedLine] end].y);
	CGContextStrokePath(context);
}

}

-(void)clearAll {
// Clear the collections
[linesInProcess removeAllObjects];
[completeLines removeAllObjects];

// Redraw
[self setNeedsDisplay];

}

@end[/code]

CPViewController.h (ColorPickerViewController)

[code]#import <UIKit/UIKit.h>

@class CPViewController;

@protocol CPViewControllerDelegate
-(void)setLineColor:(CPViewController *)controller color:(UIColor *)clr;
@end

@interface CPViewController : UIViewController {
id delegate;
}

@property (nonatomic, weak) IBOutlet UISegmentedControl *colorPicker;
@property (nonatomic, assign) id delegate;
@property (nonatomic) NSInteger selectedSegment;

-(IBAction)colorHasBeenChosen:(id)sender;

@end[/code]

CPViewController.m

[code]#import “CPViewController.h”

@implementation CPViewController

@synthesize colorPicker, selectedSegment, delegate = _delegate;

  • (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
    {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
    // Custom initialization
    }
    return self;
    }

  • (void)viewDidLoad
    {
    [super viewDidLoad];
    [colorPicker setSelectedSegmentIndex:selectedSegment];
    // Do any additional setup after loading the view from its nib.
    }

  • (void)viewDidUnload
    {
    [self setColorPicker:nil];
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    }

  • (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
    {
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
    }

-(IBAction)colorHasBeenChosen:(id)sender {
UIColor *clr;

switch([sender selectedSegmentIndex]) {
	case 0:
		clr = [UIColor redColor];
		break;
	case 1:
		clr = [UIColor greenColor];
		break;
	case 2:
		clr = [UIColor blueColor];
		break;
}

[[self delegate] setLineColor:self color:clr];
[self dismissModalViewControllerAnimated:YES];

}

@end[/code]

and a simple CPViewController.xib

IBOutlet colorPicker and IBAction colorHasBeenChosen:

As always, I’m sure there are better ways to do this challenge, but this eventually worked for me. Had me stumped for a while though :slight_smile:


#2

@TheEskil You can save a few steps if you add the segmented controller programmatically to the existing view.
Added the lineColor property to Line.h:

Added the segmentedControl ivar and selectedColor property to TouchDrawView.h:

UISegmentedControl *segmentedControl; } @property (nonatomic) UIColor *selectedColor;
Added swipe gesture recognizers to present/dismiss the color picker in TouchDrawView.m:

[code]UISwipeGestureRecognizer *swipeUpRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(presentColorPicker:)];
[swipeUpRecognizer setDirection:UISwipeGestureRecognizerDirectionUp];
[swipeUpRecognizer setNumberOfTouchesRequired:3];
[self addGestureRecognizer:swipeUpRecognizer];

UISwipeGestureRecognizer *swipeDownRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(dismissColorPicker:)];
[swipeDownRecognizer setDirection:UISwipeGestureRecognizerDirectionDown];
[swipeDownRecognizer setNumberOfTouchesRequired:3];
[self addGestureRecognizer:swipeDownRecognizer];[/code]
Added changeColor method to TouchDrawView.m:

- (void)changeColor:(UISegmentedControl *)sender { NSArray *colorArray = [NSArray arrayWithObjects:[UIColor redColor], [UIColor greenColor], [UIColor blueColor], [UIColor blackColor], nil]; [self setSelectedColor:[colorArray objectAtIndex:[sender selectedSegmentIndex]]]; }
Added methods to present/dismiss the color picker in TouchDrawView.m:

[code]- (void)presentColorPicker:(UISwipeGestureRecognizer *)gr
{
[linesInProcess removeAllObjects];

NSArray *rgbbArray = [[NSArray alloc] initWithObjects:@"Red", @"Green", @"Blue", @"Black", nil];
segmentedControl = [[UISegmentedControl alloc] initWithItems:rgbbArray];

[segmentedControl setCenter:CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)];
[segmentedControl setSegmentedControlStyle:UISegmentedControlStyleBordered];

[segmentedControl addTarget:nil
                     action:@selector(changeColor:)
           forControlEvents:UIControlEventValueChanged];

[self addSubview:segmentedControl];

}

  • (void)dismissColorPicker:(UISwipeGestureRecognizer *)gr
    {
    [linesInProcess removeAllObjects];
    [segmentedControl removeFromSuperview];
    }
    [/code]
    Set the line color in drawRect:

[code]for (Line *line in completeLines) {
if (![line lineColor])
[line setLineColor:selectedColor];

    [[line lineColor] set];[/code]


#3

I decided to display the color selection palette as a toolbar that I created as a xib in interface builder… just a simple UIToolbar in which I dropped several UIBarButtonItems with no text and different tint colors - UIBarButtonItems have a ‘tintColor’ property. (This is the property that I used to set the ‘color’ property that I added to the Line class, as the previous posters did.)

I set the TouchDrawView class to be the owner of my “ColorSelector.xib” file, created an IBOutlet reference of type UIToolbar called “overlay” and set it to point to the UIToolbar object in the xib.

I loaded the xib in the initWithFrame: method of TouchDrawView (although in retrospect viewDidLoad: seems like a better place) through the message [[NSBundle mainBundle] loadNibNamed:@"ColorSelector" owner:self options:nil]; added the overlay object as a subview, set its frame and set its ‘hidden’ property to YES. I toggled this property to display the toolbar on the three finger upward swipe gesture, and set it to be hidden again every time the user selected a color. (As you can guess, clicking on any of the bar button items triggered a common target-action which got the sending button’s ‘tintColor’ property)

Nothing special, but I think it looks kinda neat and I figured some people might like to try this approach - using an Interface Builder UI element independently of a view controller.


#4

This solution really intrigues me but after following your descriptive steps I’m not getting a toolbar. Can you post a more code specific response so I can see what I’m missing?


#5

Hey @mindblah, that looks great! Inspiring. I did it programatically rather than using a XIB, but same thing. I did it using the hidden property, as @mindblah did, but then changed it to animated for fun.

(By the way, part of me felt like I should be able to use a custom view rather than a UIToolbar, or that I should be using custom bar button item views rather than nil-title standard buttons. I tried for a while, but it’s just not very easy to get that recessed button look, background colors that change when you tap on them, rounded rectangles for buttons, a cool translucent background, etc. So UIToolbar it is!)

@marcak, I had problems getting the toolbar to show up, also. I don’t know if my troubles are the same as yours, but here were two problems (and solutions):

[ol]
[li] I had troubles recognizing the swipe gesture. Turned out I was failing to allow the swipe to be be recognized simultaneously with the pan gesture in gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:.[/li]
[li] I had troubles with laying out the toolbar frame relative to the TouchDrawView: I kept accidentally ending up with a zero-height frame, or off-screen frame. I’m not totally satisfied with my solution, but the key for me ended up being to provide a “correct” frame size for TouchDrawView’s initWithFrame:, even before autoresizing kicked in. [/li][/ol]

Anyway, here’s my code. Please forgive the style differences from the book (I use dot notation for properties).

In TouchDrawView.m, stuff to create/show/hide the palette:

- (void)BNR_initPalette
{
  NSArray *colors = [NSArray arrayWithObjects:
                     [UIColor redColor],
                     [UIColor orangeColor],
                     [UIColor yellowColor],
                     [UIColor greenColor],
                     [UIColor blueColor],
                     [UIColor purpleColor],
                     [UIColor brownColor],
                     [UIColor blackColor],
                     [UIColor grayColor],
                     [UIColor whiteColor],
                     nil];
  NSMutableArray *paletteItems = [[NSMutableArray alloc] init];
  [paletteItems addObject:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]];
  for (UIColor *color in colors) {
    UIBarButtonItem *b = [[UIBarButtonItem alloc] initWithTitle:nil
                                                          style:UIBarButtonItemStyleBordered
                                                         target:self
                                                         action:@selector(paletteButtonTapped:)];
    b.tintColor = color;
    [paletteItems addObject:b];
  }
  [paletteItems addObject:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]];
  
  _paletteView = [[UIToolbar alloc] initWithFrame:CGRectMake(self.bounds.origin.x,
                                                             self.bounds.origin.y + self.bounds.size.height - 49.0,
                                                             self.bounds.size.width,
                                                             49.0)];
  _paletteView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
  _paletteView.barStyle = UIBarStyleBlack;
  _paletteView.translucent = YES;
  _paletteView.items = paletteItems;
}

- (void)BNR_showPalette
{
  if (!_paletteView) {
    [self BNR_initPalette];
    // (If palette is not new, by the way, we trust to autoresize to keep it
    // in the correct offscreen position no matter what the current view bounds.)
    CGRect offScreen = CGRectMake(self.bounds.origin.x,
                                  self.bounds.origin.y + self.bounds.size.height,
                                  self.bounds.size.width,
                                  49.0);
    _paletteView.frame = offScreen;
    [self addSubview:_paletteView];
  }

  CGRect onScreen = CGRectMake(self.bounds.origin.x,
                               self.bounds.origin.y + self.bounds.size.height - 49.0,
                               self.bounds.size.width,
                               49.0);
  [UIView animateWithDuration:0.3 animations:^{ _paletteView.frame = onScreen; }];
}

- (void)BNR_hidePalette
{
  CGRect offScreen = CGRectMake(self.bounds.origin.x,
                                self.bounds.origin.y + self.bounds.size.height,
                                self.bounds.size.width,
                                49.0);
  [UIView animateWithDuration:0.3 animations:^{ _paletteView.frame = offScreen; }];
}

In initWithFrame:, I added the swipe up and swipe down gesture recognizers, and then just called the above show/hide methods from their action methods.

And as I mentioned, I had to make sure that the frame size passed into initWithFrame: was “correct”. This is unsatisfying to me because at view load time I feel like we shouldn’t make any assumptions about how big the view will finally appear. And yet, telling the view to show its palette during viewDidAppear didn’t seem right either. Well, it works, anyway, and if I have problems with iPad or horizontal lauching or something, then I guess I’ll work on it more.

In TouchViewController.m’s loadView:

  TouchDrawView *draw = [[TouchDrawView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

All the time working on this, as others have also mentioned, I was trying to think whether some or all of this gesture stuff should be in the view controller rather than the view.


#6

For the color picker, I opted to go the route of three UISliders, for each of red, green, and blue. I also added a swatch (UIButton) and “Set” button. This allows the user to mix any color of their choosing, without being limited to pre-defined colors.

One of the challenges I ran into was getting the individual R/G/B values out of a UIColor, which is needed when adjusting a single slider apart from the others, and being able to set those sliders when the color mixer view is loaded. The simple approach I ended up taking was having three accessors of type CGFloat, for each of the colors. I can then later combine those together in a colorWithRed:green:blue:.

Source code in its entirety:

TouchDrawView.h
Points of interest: Colors, ColorPickerViewController

#import <Foundation/Foundation.h>
#import "ColorPickerViewController.h"
@class Line;

@interface TouchDrawView : UIView <UIGestureRecognizerDelegate>
{
	Line *inProcess;
	NSMutableArray *completedLines;
	
	CGFloat maxVelocity;
	CGFloat redColor;
	CGFloat greenColor;
	CGFloat blueColor;
	UIColor *lineColor;
	
	ColorPickerViewController *cpvc;
	UIPanGestureRecognizer *moveRecognizer;
}
@property (nonatomic, weak) Line *selectedLine;

- (Line *)lineAtPoint:(CGPoint)p;
- (void)clearAll;

@end

TouchDrawView.m
Points of interest: initWithFrame: drawLine: setColor: drawRect:

#import "TouchDrawView.h"
#import "Line.h"
#import "ColorPickerViewController.h"

@implementation TouchDrawView
@synthesize selectedLine;

- (id)initWithFrame:(CGRect)r
{
	self = [super initWithFrame:r];
	
	if (self) {
		completedLines = [[NSMutableArray alloc] init];
		[self setBackgroundColor:[UIColor whiteColor]];
		[self setMultipleTouchEnabled:YES];
		
		redColor = 0.0;
		greenColor = 0.0;
		blueColor = 0.0;
		
		UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
		[tapRecognizer setNumberOfTouchesRequired:1];
		[self addGestureRecognizer:tapRecognizer];
		
		UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
		[doubleTapRecognizer setNumberOfTouchesRequired:2];
		[self addGestureRecognizer:doubleTapRecognizer];
		
		UILongPressGestureRecognizer *pressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
		[self addGestureRecognizer:pressRecognizer];
		
		moveRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(moveLine:)];
		[moveRecognizer setDelegate:self];
		[moveRecognizer setCancelsTouchesInView:NO];
		[self addGestureRecognizer:moveRecognizer];
		
		UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(drawLine:)];
		[self addGestureRecognizer:panRecognizer];
	}
	
	return self;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
	if (gestureRecognizer == moveRecognizer) {
		return YES;
	}
	return NO;
}

- (void)doubleTap:(UIGestureRecognizer *)gr
{
	inProcess = nil;
	[self clearAll];
	[self setNeedsDisplay];
}

- (void)tap:(UIGestureRecognizer *)gr
{
	CGPoint point = [gr locationInView:self];
	[self setSelectedLine:[self lineAtPoint:point]];
	
	// If we just tapped, remove all lines in process so that a tap doesn't result in a new line
	inProcess = nil;
	
	if ([self selectedLine]) {
		[self becomeFirstResponder];
		UIMenuController *menu = [UIMenuController sharedMenuController];
		UIMenuItem *deleteItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(deleteLine:)];
		[menu setMenuItems:[NSArray arrayWithObjects:deleteItem, nil]];
		
		// Tell the menu where it should come from and show it
		[menu setTargetRect:CGRectMake(point.x, point.y, 2, 2) inView:self];
		[menu setMenuVisible:YES animated:YES];
	}
	else {
		// Hide the menu if no line is selected
		[[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
	}
	
	[self setNeedsDisplay];
}

- (void)longPress:(UIGestureRecognizer *)gr
{
	if ([gr state] == UIGestureRecognizerStateBegan) {
		CGPoint point = [gr locationInView:self];
		[self setSelectedLine:[self lineAtPoint:point]];
		
		if ([self selectedLine]) {
			inProcess = nil;
		}
	}
	else if ([gr state] == UIGestureRecognizerStateEnded) {
		[self setSelectedLine:nil];
	}
	[self setNeedsDisplay];
}

- (void)moveLine:(UIPanGestureRecognizer *)gr
{
	// If we haven't selected a line, don't do anything
	if (![self selectedLine]) {
		return;
	}
	
	// When the pan recognizer changes its position...
	if ([gr state] == UIGestureRecognizerStateChanged) {
		// How far has the pan moved?
		CGPoint translation = [gr translationInView:self];
		
		// Add the translation to the current begin and end points of the line
		CGPoint begin = [[self selectedLine] begin];
		CGPoint end = [[self selectedLine] end];
		begin.x += translation.x;
		begin.y += translation.y;
		end.x += translation.x;
		end.y += translation.y;
		
		// Set the new beginning and end points of the line
		[[self selectedLine] setBegin:begin];
		[[self selectedLine] setEnd:end];
		
		[self setNeedsDisplay];
		
		// Reset the translation back to 0
		[gr setTranslation:CGPointZero inView:self];
	}
}

- (void)drawLine:(UIGestureRecognizer *)gr
{
	UIPanGestureRecognizer *pan = (UIPanGestureRecognizer*)gr;
	
	if ([pan numberOfTouches] == 3) {
		inProcess = nil;
		
		if ([pan translationInView:self].y < -50.0) {
			cpvc = [[ColorPickerViewController alloc] init];
			[cpvc setRedColor];
			[cpvc setGreenColor:greenColor];
			[cpvc setBlueColor:blueColor];
			[cpvc setDismissBlock:^{
				[self setColor];
			}];
			
			[[[self window] rootViewController] presentViewController:cpvc animated:YES completion:nil];
		}
	}
	else {
		CGPoint v = [pan velocityInView:self];
		if (v.x > maxVelocity) {
			maxVelocity = v.x;
		}
		if (v.y > maxVelocity) {
			maxVelocity = v.y;
		}
		
		if ([gr state] == UIGestureRecognizerStateBegan) {
			[[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
			selectedLine = nil;
			
			CGPoint loc = [gr locationInView:self];
			Line *newLine = [[Line alloc] init];
			[newLine setBegin:loc];
			[newLine setEnd:loc];
			[newLine setColor:lineColor];
			inProcess = newLine;
		}
		else if ([gr state] == UIGestureRecognizerStateChanged) {
			Line *line = inProcess;
			CGPoint loc = [gr locationInView:self];
			[line setEnd:loc];
			[line setVelocity:maxVelocity];
			
			[self setNeedsDisplay];
		}
		else if ([gr state] == UIGestureRecognizerStateEnded || [gr state] == UIGestureRecognizerStateFailed || [gr state] == UIGestureRecognizerStateCancelled) {
			if (inProcess) {
				[inProcess setVelocity:maxVelocity];
				[completedLines addObject:inProcess];
				inProcess = nil;
			}
			
			maxVelocity = 0.0;
			[self setNeedsDisplay];
		}
	}
}

- (void)deleteLine:(id)sender
{
	// Remove the selected line from the list of completeLines
	[completedLines removeObject:[self selectedLine]];
	[self setNeedsDisplay];
}

- (void)setColor
{
	redColor = [cpvc redColor];
	greenColor = [cpvc greenColor];
	blueColor = [cpvc blueColor];
	lineColor = [UIColor colorWithRed:redColor green:greenColor blue:blueColor alpha:1.0];
}

- (void)drawRect:(CGRect)rect
{
	CGContextRef context = UIGraphicsGetCurrentContext();
	CGContextSetLineWidth(context, 10.0);
	CGContextSetLineCap(context, kCGLineCapRound);
	
	// Draw complete lines
	for (Line *line in completedLines) {
		[[line color] set];
		CGContextSetLineWidth(context, [self getLineWidth:[line velocity]]);
		CGContextMoveToPoint(context, [line begin].x, [line begin].y);
		CGContextAddLineToPoint(context, [line end].x, [line end].y);
		CGContextStrokePath(context);
	}
	
	if (inProcess) {
		[[inProcess color] set];
		CGContextSetLineWidth(context, [self getLineWidth:[inProcess velocity]]);
		CGContextMoveToPoint(context, [inProcess begin].x, [inProcess begin].y);
		CGContextAddLineToPoint(context, [inProcess end].x, [inProcess end].y);
		CGContextStrokePath(context);
	}
	
	// Draw selected line in green
	if ([self selectedLine]) {
		[[UIColor greenColor] set];
		CGContextSetLineWidth(context, [self getLineWidth:[[self selectedLine] velocity]]);
		CGContextMoveToPoint(context, [[self selectedLine] begin].x, [[self selectedLine] begin].y);
		CGContextAddLineToPoint(context, [[self selectedLine] end].x, [[self selectedLine] end].y);
		CGContextStrokePath(context);
	}
}

- (CGFloat)getLineWidth:(CGFloat)v
{
	CGFloat width = v / 100;
	if (width < 1) {
		width = 1;
	}
	return width;
}

- (Line *)lineAtPoint:(CGPoint)p
{
	// Find a line close to p
	for (Line *l in completedLines) {
		CGPoint start = [l begin];
		CGPoint end = [l end];
		
		// Check a few points on the line
		for (float t = 0.0; t <= 1.0; t += 0.05) {
			float x = start.x + t * (end.x - start.x);
			float y = start.y + t * (end.y - start.y);
			
			// If the tapped point is within 20 points, let's return this line
			if (hypot(x - p.x, y - p.y) < 20.0) {
				return l;
			}
		}
	}
	
	// If nothing is close enough to the tapped point, then we didn't select a line
	return nil;
}

- (void)clearAll
{
	// Clear the collections
	inProcess = nil;
	redColor = 0.0;
	blueColor = 0.0;
	greenColor = 0.0;
	lineColor = [UIColor colorWithRed:redColor green:greenColor blue:blueColor alpha:1.0];

	inProcess = nil;
	[completedLines removeAllObjects];
	
	// Redraw
	[self setNeedsDisplay];
}

- (BOOL)canBecomeFirstResponder
{
	return YES;
}

@end

Line

// Add this in header, then synthesize in implementation
@property (nonatomic) UIColor *color;

ColorPickerViewController.h

#import <UIKit/UIKit.h>

@interface ColorPickerViewController : UIViewController
{
	__weak IBOutlet UISlider *redSlider;
	__weak IBOutlet UISlider *greenSlider;
	__weak IBOutlet UISlider *blueSlider;
	__weak IBOutlet UIButton *swatchView;
}

@property (nonatomic, assign) CGFloat redColor;
@property (nonatomic, assign) CGFloat greenColor;
@property (nonatomic, assign) CGFloat blueColor;
@property (nonatomic, copy) void (^dismissBlock)(void);

- (IBAction)changeRed:(UISlider *)sender;
- (IBAction)changeGreen:(UISlider *)sender;
- (IBAction)changeBlue:(UISlider *)sender;
- (IBAction)setButton:(id)sender;

@end

ColorPickerViewController.m

#import "ColorPickerViewController.h"

@implementation ColorPickerViewController
@synthesize redColor, greenColor, blueColor, dismissBlock;

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
	[self setColor];
}

- (IBAction)changeRed:(UISlider *)sender
{
	redColor = [sender value];
	[self setColor];
}

- (IBAction)changeGreen:(UISlider *)sender
{
	greenColor = [sender value];
	[self setColor];
}

- (IBAction)changeBlue:(UISlider *)sender
{
	blueColor = [sender value];
	[self setColor];
}

- (IBAction)setButton:(id)sender
{
	[[self presentingViewController] dismissViewControllerAnimated:YES completion:dismissBlock];
}

- (void)setColor
{
	[redSlider setValue];
	[greenSlider setValue:greenColor];
	[blueSlider setValue:blueColor];
	[swatchView setBackgroundColor:[UIColor colorWithRed:redColor green:greenColor blue:blueColor alpha:1.0]];
}

@end

ColorPickerViewController.xib
[i] - Min Track Tint colors: 255,0,0

  • Max Track Tint colors: 255,200,200
  • Swatch is just a UIButton with “Type” set to “Custom”. Remove text.
    [/i]