Silver & Gold


#1

As in another post, I’m saving the bronze challenge till I’ve done a bit more on archiving/state preservation, unless I’m missing something simple maybe?
Silver:
include the math library and…

#import "BNRLine.h"
#include <math.h>


@implementation BNRLine

-(UIColor *)lineColor
{
//Get floats for the coordinate differences
    CGFloat yDim=self.end.y-self.begin.y;
    CGFloat xDim=self.end.x-self.begin.x;
    
//Get line angle from the inverse tan
    CGFloat angle=atan2f(xDim, yDim);
    
//Get a value from 0-1 from the angle in radians
    CGFloat cValue=(angle+M_PI)/(M_PI*2);
    
//Set the hue value of the line colour
    UIColor *color=[UIColor colorWithHue:cValue saturation:1.0 brightness:1.0 alpha:1.0];
    
//return the colour
    return color;
}
@end

Then the stroke colour is set in the view controller.

Gold:
I’m not sure this was the intended way, but it works quite well on top of the existing line drawing functionality, except that the the lines are always drawn first, so always appear underneath. I think this could be fixed quite easily but… later!
I add a pinch gesture recogniser, set its target to an action that instantiates a circle object from the scale and location of the pinch, and if the pinch ends the penultimate circle is added to the circles array.
I couldn’t see an obvious need or way to add the circlesInProgress to a dictionary… maybe someone could explain why this might be a better choice somehow.
The circle colours are randomly generated and it looks quite psychedelic as you pinch them out.
Here’s the whole of DrawView


#import "BNRDrawView.h"
#import "BNRLine.h"
#import "BNRCircle.h"

@interface BNRDrawView ()

@property (nonatomic, strong)NSMutableDictionary *linesInProgress;
@property (nonatomic, strong)NSMutableArray *finishedLines;
//New property for circle as it is being set with pinch
@property (nonatomic, strong)BNRCircle *circleInProgress;
//New array for circles once they have been set
@property (nonatomic, strong)NSMutableArray *finishedCircles;

@end

@implementation BNRDrawView

-(instancetype)initWithFrame:(CGRect)r
{
    self=[super initWithFrame:r];
    
    if (self) {
        
        self.linesInProgress=[[NSMutableDictionary alloc]init];
        self.finishedLines=[[NSMutableArray alloc]init];
        self.finishedCircles=[[NSMutableArray alloc]init];
        
        
        self.backgroundColor = [UIColor grayColor];
        self.multipleTouchEnabled=YES;
//Get a pinch gesture recognizer
        UIPinchGestureRecognizer *pgr=[[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(respondsToPinch:)];
//add it to the view
        [self addGestureRecognizer:pgr];
        
    }
    
    return self;
}

-(void)strokeLine:(BNRLine *)line
{
    UIBezierPath *bp=[UIBezierPath bezierPath];
    bp.lineWidth=10;
    bp.lineCapStyle=kCGLineCapRound;
    
    [bp moveToPoint:line.begin];
    [bp addLineToPoint:line.end];
    [bp stroke];
}

//Method to stroke a circle from a given rect
-(void)strokeCircleInRect:(CGRect)r
{
    UIBezierPath *bp=[UIBezierPath bezierPathWithOvalInRect:r];
    bp.lineWidth=10;
    [bp stroke];
    
}

-(void)drawRect:(CGRect)rect
{
//Draw finished lines using lineColor property
    for (BNRLine *line in self.finishedLines) {
        [line.lineColor set];
        [self strokeLine:line];
    }
//Draw lines in progress in red
    [[UIColor redColor]set];
    for (NSValue *key in self.linesInProgress){
        [self strokeLine:self.linesInProgress[key]];
    }
//Draw finished circles in circleColors
    for(BNRCircle *circle in self.finishedCircles){
        [circle.circleColor set];
        [self strokeCircleInRect:circle.circleRect];
    }
//Draw circleInProgress in circlecolors
    [self.circleInProgress.circleColor set];
    [self strokeCircleInRect:self.circleInProgress.circleRect];
   }

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{ 
    NSLog(@"%@", NSStringFromSelector(_cmd));

    for (UITouch *t in touches){
        CGPoint location=[ t locationInView:self];
        BNRLine *line=[[BNRLine alloc ]init];
         line.begin=location;
         line.end=location;
        NSValue *key=[NSValue valueWithNonretainedObject:t];
       self.linesInProgress[key]=line;      
    }
    [self setNeedsDisplay];
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@",NSStringFromSelector(_cmd));
    for (UITouch *t in touches){
        NSValue *key=[NSValue valueWithNonretainedObject:t];
        BNRLine *line = self.linesInProgress[key];
        line.end = [t locationInView:self];
    }
    [self setNeedsDisplay];
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@",NSStringFromSelector(_cmd));
    for (UITouch *t in touches){
        NSValue *key=[NSValue valueWithNonretainedObject:t];
        BNRLine *line=self.linesInProgress[key];
        [self.finishedLines addObject:line];
        [self.linesInProgress removeObjectForKey:key];
    }
    [self setNeedsDisplay];
}

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@",NSStringFromSelector(_cmd));
    for (UITouch *t in touches){
        NSValue *key=[NSValue valueWithNonretainedObject:t];
        [self.linesInProgress removeObjectForKey:key];
    }
    [self setNeedsDisplay];
}

-(void)respondsToPinch:(UIPinchGestureRecognizer*)gr
{
//Get a center point from the pinch
    CGPoint center=[gr locationInView:self];
    
//Get a 'radius', or actually the sidelength of a square rect, from a product of the pinch.scale
    CGFloat radius=gr.scale*100;
    
//move the origin over by half the rect dimensions
    center.x-=radius/2;
    center.y-=radius/2;
    
//Get the rect
    CGRect r=CGRectMake(center.x, center.y,radius,radius);
    
//Instantiate a circle object with the rect
    BNRCircle *circle=[[BNRCircle alloc]init];
    circle.circleRect=r;
    
//Generate a random colour for the circle
    uint randomHue= (arc4random()%100);
    CGFloat hue=(float)randomHue/100;
    
//Assign it to the circelcolour
    circle.circleColor=[UIColor colorWithHue:hue saturation:1.0 brightness:1.0 alpha:1.0];

//If this is the last circle made as the gesture ends...
    if (gr.state==UIGestureRecognizerStateEnded) {        
//Make the penultimate circle the finished circle – otherwise it skips as you remove one finger
        [self.finishedCircles addObject:self.circleInProgress];
    }else{
//if not update circleInProgress
        self.circleInProgress=circle;
    }
    
//Redraw
    [self setNeedsDisplay];
}

@end

#2

I didn’t use the pinch gesture recognizer for the Gold Challenge as they had not covered it at that point, and I didn’t think they intended the reader to use the gesture recognizer (that stops the challenge from begin “Gold” caliber). Instead I used a tracking mechanism in the touch event callback methods. I speculate that the authors intended to show the challenge that goes along recognizing gestures, and then in the following chapter (where they cover gestures) they make the reader recognize the great convenience of using gesture recognizers. Anyways I included my code below. To help simplify the code in the BNRDrawView, I introduced a model object to represent a circle (called BNRCircle), similar to the BNRLine design pattern. The code also includes the “Silver” challenge which is taken care of in the drawRect method. FYI you don’t need to include the math.h library in the Silver challenge as that is imported by default through Foundation.h.

#import <Foundation/Foundation.h>

@interface BNRCircle : NSObject

@property (nonatomic, readonly) CGPoint center;
@property (nonatomic, readonly) CGFloat radius;
@property (nonatomic, readonly) CGRect bounds;

// the "points" array is expected to be an array of NSValue
// objects that wraps a CGPoint
- (instancetype)initWithBoundaryPoints:(NSArray *)points;

@end



#import "BNRCircle.h"

@implementation BNRCircle

- (instancetype)initWithBoundaryPoints:(NSArray *)points
{
    self = [super init];
    
    if (self) {
        if ([points count] == 2) {
            CGPoint point1 = [points[0] CGPointValue];
            CGPoint point2 = [points[1] CGPointValue];
            
            // Get the longest side of the rectangle that is composed of
            // two points that represent opposing rectangle corner points.
            CGFloat maxSide = MAX(fabsf(point1.x - point2.x), fabsf(point1.y - point2.y));
            
            _radius = maxSide/2.0;
            
            _center.x = (MAX(point1.x, point2.x) - MIN(point1.x, point2.x))/2.0 + MIN(point2.x, point1.x);
            _center.y =  (MAX(point1.y, point2.y) - MIN(point1.y, point2.y))/2.0 + MIN(point2.y, point1.y);
            
            _bounds = CGRectMake(_center.x -_radius, _center.y - _radius, maxSide, maxSide);
        }
        else
        {
            _center = CGPointZero;
            _radius = 0;
            _bounds = CGRectZero;
        }
    }
    return self;
}


- (instancetype)init
{
    return [self initWithBoundaryPoints:nil];
}

@end



@interface BNRDrawView ()

@property (nonatomic, strong) NSMutableDictionary *linesInProgress;
@property (nonatomic, strong) NSMutableArray *finishedLines;
@property (nonatomic, strong) NSMutableDictionary *circleInProgress;
@property (nonatomic, strong) NSMutableArray *finishedCircles;

@end



///////  Implementation for BNRDrawView /////////

#import "BNRDrawView.h"
#import "BNRLine.h"
#import "BNRCircle.h"

@implementation BNRDrawView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        _linesInProgress = [[NSMutableDictionary alloc] init];
        _circleInProgress = [[NSMutableDictionary alloc] init];
        _finishedLines = [[NSMutableArray alloc] init];
        _finishedCircles = [[NSMutableArray alloc] init];
        self.backgroundColor = [UIColor grayColor];
        self.multipleTouchEnabled = YES;
    }
    return self;
}


- (void)strokeLine:(BNRLine *)line
{
    UIBezierPath *bp = [UIBezierPath bezierPath];
    bp.lineWidth = 10;
    bp.lineCapStyle = kCGLineCapRound;
    
    [bp moveToPoint:line.begin];
    [bp addLineToPoint:line.end];
    [bp stroke];
}



- (void)drawRect:(CGRect)rect
{

    for (BNRLine *line in self.finishedLines) {
        
        // Get angle of line in radian.
        // atan2f will give range between - Pi/2 to + Pi/2
        CGFloat hue = atan2f(line.end.y - line.begin.y, line.end.x - line.begin.x);
        
        hue += M_PI; // Add Pi to offset the negative angle value returned by atan2f
        hue /= M_PI * 2.0; // Divide by 2 * Pi to convert to value range from 0 to 1
        
        // Obtain the appropriate color based on line angle
        [[UIColor colorWithHue:hue saturation:1.0 brightness:1.0 alpha:1.0] set];

        [self strokeLine:line];
    }
    
    
    // If there is a line currently begin draw, do it in red
    [[UIColor redColor] set];
    
    for (NSValue *key in self.linesInProgress) {
        [self strokeLine:self.linesInProgress[key]];
    }
    
    
    // If there are any circles being drawn using two touch points,
    // draw them also in red
    if ([self.circleInProgress count] > 0) {
        
        NSArray *twoPoints = [self.circleInProgress allValues];
        BNRCircle *circle = [[BNRCircle alloc] initWithBoundaryPoints:twoPoints];
        
        UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:circle.bounds];
        [circlePath setLineWidth:10.0];
        [circlePath stroke];
    }
    
    
    [[UIColor blackColor] set];
    // Draw any completed circles in black color
    if ([self.finishedCircles count] > 0) {

        for (BNRCircle *circle in self.finishedCircles) {
            UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:circle.bounds];
            [circlePath setLineWidth:10.0];
            [circlePath stroke];
        }
    }
}


- (NSArray *)allLines
{
    return self.finishedLines;
}

- (void)setAllLines:(NSArray *)lines
{
    if (lines) {
        self.finishedLines = [lines mutableCopy];
    }
}

// Touch call back methods

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // If a touch event begins with two touches, then store them
    //in the circleInProgress dictionary, using the value of the touch
    // as the key
    if ([touches count] == 2) {
        for (UITouch *t in touches) {
            CGPoint location = [t locationInView:self];
            NSValue *key = [NSValue valueWithNonretainedObject:t];

            self.circleInProgress[key] = [NSValue valueWithCGPoint];
        }
    }
    else { // Otherwise collect the touches for line creation
        for (UITouch *t in touches) {
            // Get location o the touch in view's cordinate system
            CGPoint location = [t locationInView:self];
            
            BNRLine *line = [[BNRLine alloc] init];
            line.begin = location;
            line.end = location;
            
            NSValue *key = [NSValue valueWithNonretainedObject:t];
            self.linesInProgress[key] = line;
        }

    }
    
    [self setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    
    for (UITouch *t in touches) {
        // Get location o the touch in view's cordinate system
        CGPoint location = [t locationInView:self];
        
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        BNRLine *line = self.linesInProgress[key];
        line.end = location;
        
        // Determine if there are any pair of two points being touched
        NSValue *pointValue = self.circleInProgress[key];
        if (pointValue) {
            // Update the moved touch location to the new location
            self.circleInProgress[key] = [NSValue valueWithCGPoint];
        }
    }
    
    
    [self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{

    for (UITouch *t in touches) {
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        BNRLine *line = self.linesInProgress[key];
        
        // If not keeping track of circle creation AND a line
        // creation process had already began, then move the
        // completed line info to the array.
        if (!self.circleInProgress[key] && line) {
            [self.finishedLines addObject:line];
            [self.linesInProgress removeObjectForKey:key];
        }
        else {
            // If creating a circle, then extract the location of the two
            // touch points that were moved off the screen, and create a BNRCircle
            // object to represent the final circle.
            NSArray *twoPoints = [self.circleInProgress allValues];
            
            BNRCircle *circle = [[BNRCircle alloc] initWithBoundaryPoints:twoPoints];
            [self.finishedCircles addObject:circle];
            [self.circleInProgress removeAllObjects];
        }
    }
    
    [self setNeedsDisplay];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    
    for (UITouch *t in touches) {
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        [self.linesInProgress removeObjectForKey:key];
    }

    [self.circleInProgress removeAllObjects];

    [self setNeedsDisplay];

}
@end

#3

Yep you’re right and that’s the code I should have written. As soon as I turned the page and saw the chapter on gesture recognisers I knew I’d jumped the gun.
I then tried to generalise it a bit more and created a BNRPath object which could be a line or a circle with a colour property and a single drawing message in drawInRect. This works well because you can draw lines and circles in order – as it is either all the circles or all the lines, new or otherwise, are drawn first.
Anyway, onwards and upwards…


#4

With your help, I finally made my code work. Thanks.


@property (nonatomic, strong) NSMutableDictionary *linesInProgress;
@property (nonatomic, strong) NSMutableArray *finishedLines;

-(void)strokeCircle:(BNRLine *)line
{
    CGFloat xdiff = line.end.x - line.begin.x;
    CGFloat ydiff = line.end.y - line.begin.y;
    float radius = hypotf(fabsf(xdiff), fabsf(ydiff)) / 2;
    CGPoint center = CGPointMake(line.end.x - (xdiff / 2), line.end.y - (ydiff / 2));
    UIBezierPath *bp = [UIBezierPath bezierPathWithArcCenter:center
                                                      radius:radius
                                                  startAngle:0
                                                    endAngle:2 * M_PI
                                                   clockwise:YES];
    bp.lineWidth = 10;
    [bp stroke];
}

-(void)drawRect:(CGRect)rect
{
    [[UIColor blackColor] set];
    for (BNRLine *line in self.finishedLines) {
        [self strokeCircle:line];
    }

    [[UIColor redColor] set];
    if (self.linesInProgress.count == 2) {
        BNRLine *line = [[BNRLine alloc] init];
        NSArray *array = [self.linesInProgress allValues];
        line.begin = [array[0] CGPointValue];
        line.end = [array[1] CGPointValue];
        [self strokeCircle:line];
    }
}

-(void)touchesBegan:(NSSet *)touches
          withEvent:(UIEvent *)event
{
    for (UITouch *t in touches) {
        CGPoint point = [t locationInView:self];
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        self.linesInProgress[key] = [NSValue valueWithCGPoint:point];
    }
    
    [self setNeedsDisplay];
}

-(void)touchesMoved:(NSSet *)touches
          withEvent:(UIEvent *)event
{
    for (UITouch *t in touches) {
        CGPoint point = [t locationInView:self];
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        self.linesInProgress[key] = [NSValue valueWithCGPoint:point];
    }

    [self setNeedsDisplay];
}

-(void)touchesEnded:(NSSet *)touches
          withEvent:(UIEvent *)event
{
    if (self.linesInProgress.count == 2) {
        BNRLine *line = [[BNRLine alloc] init];
        NSArray *array = [self.linesInProgress allValues];
        line.begin = [array[0] CGPointValue];
        line.end = [array[1] CGPointValue];
        [self.finishedLines addObject:line];
    }
    [self.linesInProgress removeAllObjects];
    
    [self setNeedsDisplay];
}

#5

My god… I’m going through the back a second time around and I’ve been breezing through all of the challenges so far this time around, but this one really knocked me off my feet. And to be perfectly honest, reading through and redoing your version really does not make it any better!
I feel I’m out of my league here… I think I’m a bit stumped by the “math” done in the BNRCircle implementation file as well though. I don’t really comprehend just how the circles are drawn eventually.


#6

Adding a few more comments on my part may have helped. Giving a high level overview may help clarify the purpose of what BNRCircle is trying to do.

BNRDrawView is collecting information from your fingers. Whenever BNRDrawView encounters a set of two touches (and two touches only) it stores them in a Dictionary that will be shipped off to BNRCircle. BNRCircle is supposed to represent a Model object, however to be fair, it does a few things that a “model” object shouldn’t do, which is, converting a set of two points into a circle representation. Think of these two points as the end points of a straight line. Now think of this line as the Diameter of a circle. The math in BNRCircle attempts to find the magnitude of this diameter as well as it’s midpoint (i.e. centre of circle).

Step 1) find the distance between the two points (the diameter of a circle).
Step 2) find the midpoint of the line that connects the two points (the centre of circle).
Step 3) Use the Diameter and Centre to create a square that bounds the circle.

The radius and the centre are used to calculate the square that will become the boundary of the circle. Once you have the square (i.e. a CGRect), then drawing the circle is quite trivial due the the one line API that UIBezierPath has for creating a circle. Or once you have the radius and centre, do what sunny4s has done above, and use another API that draws a circle/arc.

Hope that helped clarify a bit.


#7

I used the rotation gesture to recognize when two lines are making a circular motion (or close to it). All I do with the rotation gesture is to flag a rotation gesture was found. Since rotation cancels the touches, I check in the cancellation function, if the count is two points being cancelled, and if it is due to rotation.

If determined a rotation, I use the beginnings of the lines cancelled to create the circle from and add it to the collection of finished circles.

The actual drawing of circles was similar to what was posted earlier in this thread.

When drawing the circles the hue is altered just like with lines, using the angle of the two points defining the circle to determine the hue change.

I step through both lines and circles at the same time drawing the first line, then the first circle, then the second line and then the second circle…etc I just used a for loop for both circles and lines.


#8

Here is a solution using techniques from mataz137’s solution, but removing the use of NSDictionary to track the circle in progress. The silver solution uses bit shifting to try to assign a one->one mapping of degrees the line is at, to the 256256256 color options.
Some thing to note
[ul]
-behaves strangely if you are drawing a circle and then begin to draw a line (with a third finger)
-has some serious lag if you are drawing a line, then a sufficient amount of time later (such that the phone doesn’t think you trying to draw a circle), draw another line.
-Although it’s possible to recycle the circle class, it is much more clear to make it its own object
-The hardest part for me was completing the gold challenge without using GestureRecognizers. When you get to the touchesEnded method, you have to account for the possibility that the user releases one point of the circle before the other.
-You are only allowed to draw one circle at a time. I think it could be possible using NSValue valueWithNonretainedObject to track touches properly to allow some dexterous person to draw two circles at once, but that can be for the iPad version, not today.
-looking at the cpu usage bar in Xcode when drawing a circle, it uses anywhere from 20% to 49%. If anyone can tell me why this is so cpu exhaustive, please do.
[/ul]

SLRCircle class

@import UIKit;

@interface SLRCircle : NSObject
@property (nonatomic) CGPoint point1;
@property (nonatomic) CGPoint point2;
@property (nonatomic, readonly) CGRect boundRect;
@end

#import "SLRCircle.h"

@implementation SLRCircle
- (CGRect)boundRect {
    if (!self.point1.x & !self.point2.x) {
        return CGRectNull;
    }
    CGFloat x,y,w,h;
    x = MIN(self.point1.x, self.point2.x);
    y = MIN(self.point1.y, self.point2.y);
    w = fabs(self.point1.x - self.point2.x);
    h = fabs(self.point1.y - self.point2.y);
    return CGRectMake(x, y, w, h);
}
@end

SLRLine class


@import UIKit;

@interface SLRLine : NSObject
@property (nonatomic) CGPoint begin;
@property (nonatomic) CGPoint end;
- (NSInteger)angleToHorizontalDegrees;
@end

#import "SLRLine.h"

@implementation SLRLine
- (NSInteger)angleToHorizontalDegrees {
    CGFloat opp = fabs(self.end.y - self.begin.y);
    CGFloat adj = fabs(self.end.x - self.begin.x);
    //returns 0 - 90 degrees, can use improvement
    return round(atan(opp/adj)*180/M_PI);
}
@end

SLRDrawView class


#import "SLRDrawView.h"
#import "SLRLine.h"
#import "SLRCircle.h"

@interface SLRDrawView ()
@property (nonatomic, strong) NSMutableDictionary *linesInProgress;
@property (nonatomic, strong) NSMutableArray *finishedLines;
@property (nonatomic, strong) SLRCircle *circleInProgress;
@property (nonatomic, strong) NSMutableArray *finishedCircles;
@end

@implementation SLRDrawView

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        self.linesInProgress = [[NSMutableDictionary alloc] init];
        self.finishedLines = [[NSMutableArray alloc] init];
        self.backgroundColor = [UIColor grayColor];
        self.finishedCircles = [[NSMutableArray alloc] init];
        self.multipleTouchEnabled = YES;
    }
    return self;
}

- (void)strokeLine:(SLRLine *)line {
    UIBezierPath *bp = [UIBezierPath bezierPath];
    bp.lineWidth = 10;
    bp.lineCapStyle = kCGLineCapRound;
    [bp moveToPoint:line.begin];
    [bp addLineToPoint:line.end];
    [bp stroke];
}

- (void)strokeCircle:(SLRCircle *)circle {
    UIBezierPath *bp = [UIBezierPath bezierPathWithOvalInRect:circle.boundRect];
    bp.lineWidth = 5;
    [bp stroke];
}

- (UIColor *)lineAngleColor:(SLRLine *)line {
    NSInteger angle = [line angleToHorizontalDegrees];
    NSInteger r,g,b;
    angle = (1 << 24)/90*angle;
    r = (angle >> 16) & 0xFF;
    g = (angle >> 8) & 0xFF;
    b = (angle >> 0) & 0xFF;
    return [UIColor colorWithRed:r/256.0 green:g/256.0 blue:b/256.0 alpha:1.0];
}

- (UIColor *)circleColor:(SLRCircle *)circle {
    NSInteger radius = circle.boundRect.size.width/2;
    //change radius to be a number between 0 and 2^24 (number of possible colors). Assuming 186 is max radius
    radius = (1 << 24)/186*radius;  
    NSInteger r,g,b;
    //r g and b are all integers between 0 and 256
    r = (radius >> 16) & 0xFF;
    g = (radius >> 8) & 0xFF;
    b = (radius >> 0) & 0xFF;
    return [UIColor colorWithRed:r/256.0 green:g/256.0 blue:b/256.0 alpha:1.0];
}
- (void)drawRect:(CGRect)rect {
    for (SLRLine *line in self.finishedLines) {
        [[self lineAngleColor:line] set];
        [self strokeLine:line];
    }
    
    [[UIColor redColor] set];
    for (NSValue *key in self.linesInProgress) {
        [self strokeLine:self.linesInProgress[key]];
    }
    
    for (SLRCircle *c in self.finishedCircles) {
        [[self circleColor:c] set];
        [self strokeCircle:c];
    }
    
    [[UIColor blueColor] set];
    if (self.circleInProgress) {
        [self strokeCircle:self.circleInProgress];
    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([touches count] != 2){
        for (UITouch *t in touches) {
            CGPoint location = [t locationInView:self];
            SLRLine *line = [[SLRLine alloc] init];
            line.begin = location;
            line.end = location;
            NSValue *key = [NSValue valueWithNonretainedObject:t];
            self.linesInProgress[key] = line;
        }
    } else {
        NSArray *tArray = [touches allObjects];
        SLRCircle *circle = [[SLRCircle alloc] init];
        circle.point1 = [tArray[0]  locationInView:self];
        circle.point2 = [tArray[1] locationInView: self];
        self.circleInProgress = circle;
    }
    [self setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([touches count] != 2) {
        for (UITouch *t in touches) {
            NSValue *key = [NSValue valueWithNonretainedObject:t];
            SLRLine *line = self.linesInProgress[key];
            line.end = [t locationInView:self];
        }
    } else {
        NSArray *tArray = [touches allObjects];
        SLRCircle *circle = self.circleInProgress;
        circle.point1 = [tArray[0]  locationInView:self];
        circle.point2 = [tArray[1] locationInView: self];
    }
    [self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *t in touches) {
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        SLRLine *line = self.linesInProgress[key];
        //if not creating a circle and a line is in progress
        if (!self.circleInProgress && line) {
            [self.finishedLines addObject:line];
            [self.linesInProgress removeObjectForKey:key];
        } else if (self.circleInProgress) { //if creating a circle, grab the points and move the circle to completed array
            [self.finishedCircles addObject:self.circleInProgress];
            self.circleInProgress = nil;
        }
    }
    [self setNeedsDisplay];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    
    for (UITouch *t in touches) {
        NSValue *key = [NSValue valueWithNonretainedObject:t];
        [self.linesInProgress removeObjectForKey:key];
    }
    self.circleInProgress = nil;
    [self setNeedsDisplay];
}
@end

#9

Here’s my solution. I implemented the Gold challenge to use two fingers to shape the corners of the circle’s bounding box. If the corners are too close together, then the distance of the fingers determines the radius of the circle. This way you can zoom and pinch and rotate your two fingers around the circle on the screen. It’s pretty cool!

As far as finishing a circle, my design decision was that once a circle is started, lifting one finger will finish the circle. The second finger’s touchesEnded:withEvents: message will just go unhandled.

The color and line width/circle stroke vary depending on the size of the circle or length of the line.

My biggest weakness in the design (to self-critique) is that I should have make a Circle class instead. There was too much work converting a line to a Corner class, then extracting the corner’s point etc etc. I’ve been working on the challenge at 6 AM, so I was groggy and committed to making my bad design work. :smiley:

Without further ado:

//
//  BNRDrawView.m
//  TouchTracker
//

#import "BNRDrawView.h"
#import "BNRLine.h"
#import "BNRCorner.h"

@interface BNRDrawView ()

@property (nonatomic, strong) NSMutableDictionary *currentLines;
@property (nonatomic, strong) NSMutableArray *finishedLines;

@property (nonatomic, strong) NSMutableDictionary *currentCircleCorners;
@property (nonatomic, strong) NSMutableArray *finishedCircles;

@end

CGRect CGRectMakeRectFromPoints(CGPoint p1, CGPoint p2)
{
    return CGRectZero;
}

@implementation BNRDrawView

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        self.currentLines = [NSMutableDictionary dictionary];
        self.finishedLines = [NSMutableArray array];
        
        self.currentCircleCorners = [NSMutableDictionary dictionary];
        self.finishedCircles = [NSMutableArray array];
        
        self.backgroundColor = [UIColor blackColor];
        self.multipleTouchEnabled = YES;
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    [[UIColor blackColor] set];
    
    for (BNRLine *line in self.finishedLines)
    {
        [[self strokeColorForAngledLine:line] set];
        [self strokeLine:line];
    }
    
    for (NSValue *key in self.currentLines)
    {
        BNRLine *line = self.currentLines[key];
        
        [[self strokeColorForAngledLine:line] set];
        [self strokeLine:self.currentLines[key]];
    }
    
    for (NSArray *circleCorners in self.finishedCircles)
    {
        [self drawCircleWithCorners:circleCorners];
    }
    
    if (self.currentCircleCorners.count == 2)
    {
        [self drawCircleWithCorners:[self getCorners:self.currentCircleCorners]];
    }
}

#pragma mark - UIResponder Methods

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    
    for (UITouch *touch in touches)
    {
        CGPoint location = [touch locationInView:self];
        
        BNRLine *line = [[BNRLine alloc] init];
        line.begin = location;
        line.end = location;
        
        NSValue *key = [NSValue valueWithNonretainedObject:touch];
        self.currentLines[key] = line;
    }
    
    [self moveLinesToCirclesIfNeeded];
    
    [self setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    
    for (UITouch *touch in touches)
    {
        NSValue *key = [NSValue valueWithNonretainedObject:touch];
        
        BNRLine *line;
        if (self.currentCircleCorners[key])
        {
            line = self.currentCircleCorners[key];
        }
        else if (self.currentLines[key])
        {
            line = self.currentLines[key];
        }
        else
        {
            // touch ended after the first of the two touches for a circle ended.
            // therefore, it has been already removed from the tracking dictionary
        }
        
        line.end = [touch locationInView:self];
    }
    
    [self moveLinesToCirclesIfNeeded];

    [self setNeedsDisplay];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    
    for (UITouch *touch in touches)
    {
        NSValue *key = [NSValue valueWithNonretainedObject:touch];
        
        BNRLine *line;
        if ((line = self.currentLines[key]))
        {
            [self.finishedLines addObject:line];
            [self.currentLines removeObjectForKey:key];
        }
        else if ((line = self.currentCircleCorners[key]))
        {
            NSMutableArray *completedCorners = [NSMutableArray array];
            for (NSValue *key in [self.currentCircleCorners copy])
            {
                [completedCorners addObject:[self cornerForLine:self.currentCircleCorners[key]]];
                [self.currentCircleCorners removeObjectForKey:key];
            }
            [self.finishedCircles addObject:completedCorners];
            break;
        }
        else
        {
            // touch ended after the first of the two touches for a circle ended.
            // therefore, it has been already removed from the tracking dictionary
        }
    }
    
    [self setNeedsDisplay];
}

- (BNRCorner *)cornerForLine:(BNRLine *)line
{
    return [BNRCorner cornerWithPoint:line.end];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@", NSStringFromSelector(_cmd));

    [self.currentLines removeAllObjects];
    [self.currentCircleCorners removeAllObjects];
    
    [self setNeedsDisplay];
}

#pragma mark - Drawing Methods

- (void)strokeLine:(BNRLine *)line
{
    UIBezierPath *path = [UIBezierPath bezierPath];
    
    CGFloat x = MAX(line.begin.x, line.end.x) - MIN(line.begin.x, line.end.x);
    CGFloat y = MAX(line.begin.y, line.end.y) - MIN(line.begin.y, line.end.y);
    
    CGFloat lineLength = hypotf(x, y);
    
    path.lineWidth = MAX(lineLength / 50, 1);
    path.lineCapStyle = kCGLineCapRound;
    
    [path moveToPoint:line.begin];
    [path addLineToPoint:line.end];
    
    [path stroke];
}

- (void)drawCircleWithCorners:(NSArray *)corners
{
    BNRCorner *c1 = corners[0];
    BNRCorner *c2 = corners[1];
    
    CGPoint center;
    center.x = (MAX(c1.point.x, c2.point.x) + MIN(c1.point.x, c2.point.x)) / 2.0f;
    center.y = (MAX(c1.point.y, c2.point.y) + MIN(c1.point.y, c2.point.y)) / 2.0f;
    
    CGFloat radius = MAX(MAX(c1.point.y, c2.point.y) - center.y, MAX(c1.point.x, c2.point.x) - center.x);
    
    UIBezierPath *circle = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0.0F endAngle:2.0F * M_PI clockwise:YES];
    circle.lineWidth = MAX(ceilf(radius/20), 3);
    circle.lineCapStyle = kCGLineCapRound;
    
    [[self strokeColorForMeasurement:radius] set];
    
    [circle stroke];
}

#pragma mark - Helper Methods

- (NSArray *)getCorners:(NSMutableDictionary *)corners
{
    if (corners.count != 2)
    {
        return nil;
    }
    
    CGPoint c1, c2;
    
    NSMutableArray *array = [NSMutableArray array];
    NSInteger idx = 0;
    for (NSValue *key in corners)
    {
        array[idx++] = corners[key];
    }
    
    c1 = ((BNRLine*)array[0]).end;
    c2 = ((BNRLine*)array[1]).end;
    
    BNRCorner *corner1 = [BNRCorner cornerWithPoint:c1];
    BNRCorner *corner2 = [BNRCorner cornerWithPoint:c2];
    
    return @[corner1, corner2];
}

- (void)moveLinesToCirclesIfNeeded
{
    if (self.currentLines.count == 2)
    {
        for (NSValue *key in [self.currentLines copy])
        {
            self.currentCircleCorners[key] = self.currentLines[key];
            [self.currentLines removeObjectForKey:key];
        }
    }
}

- (UIColor *)strokeColorForMeasurement:(CGFloat)measurement
{
    CGFloat colorValue = measurement/360;
    
    return [UIColor colorWithHue:colorValue saturation:1 brightness:1 alpha:1];
}

- (UIColor *)strokeColorForAngledLine:(BNRLine *)line
{
    return [self strokeColorForMeasurement:[line angle]];
}

@end