Gold Challenge Solution


#1

This took a little more time than I’d like to admit to, but it works.

So, first, TouchDrawView.h needs another dictionary and an array for circles.

[code]@interface TouchDrawView : UIView
{
NSMutableDictionary *linesInProcess;
NSMutableArray *completeLines;
NSMutableDictionary *circleInProcess;
NSMutableArray *completeCircles;
}

  • (void)clearAll;[/code]

Then in TouchDrawView.m add the two lines to - (id)initWithFrame:(CGRect)r to get the process going for the dictionary and array.

linesInProcess = [[NSMutableDictionary alloc] init]; circleInProcess = [[NSMutableDictionary alloc] init]; completeLines = [[NSMutableArray alloc] init]; completeCircles = [[NSMutableArray alloc] init];

Also add the appropriate lines to the clearAll method.

- (void)clearAll { // Clear the collections [linesInProcess removeAllObjects]; [completeLines removeAllObjects]; [circleInProcess removeAllObjects]; [completeCircles removeAllObjects]; // Redraw [self setNeedsDisplay]; }

Next, update the drawRect: method to draw the circles and circlesInProcess. Each circle requires two Line objects.

[code][[UIColor cyanColor] set];
for (int i = 0; i < [completeCircles count]; i+=2) {
// Get the two points for the circle
Line *point1 = [completeCircles objectAtIndex:i];
Line *point2 = [completeCircles objectAtIndex:i+1];

    // Set the radius for the circle
    CGFloat radius = hypotf(point1.end.x - point2.end.x, point1.end.y - point2.end.y) / 2;
    CGContextAddArc(context, 
                    (point1.end.x + point2.end.x) / 2, 
                    (point1.end.y + point2.end.y) / 2, 
                    radius, 
                    0.0, 
                    2 * M_PI, 
                    YES);
    // Draw it
    CGContextStrokePath(context);
    
}

[[UIColor redColor] set];
if ([circleInProcess count] == 2) {
    // Create an array to hold the two Line objects from the dictionary
    // Probably not the most elegant solution, but it works
    NSMutableArray *circlePoints = [NSMutableArray array];
    
    for (NSValue *v in circleInProcess)
        [circlePoints addObject:[circleInProcess objectForKey:v]];
    
    Line *point1 = [circlePoints objectAtIndex:0];
    Line *point2 = [circlePoints objectAtIndex:1];
    
    CGFloat radius = hypotf(point1.end.x - point2.end.x, point1.end.y - point2.end.y) / 2;
    CGContextAddArc(context, 
                    (point1.end.x + point2.end.x) / 2, 
                    (point1.end.y + point2.end.y) / 2, 
                    radius, 
                    0.0, 
                    2 * M_PI, 
                    YES);
    CGContextStrokePath(context);
    
}[/code]

Now, in touchesBegan:withEvent: add an object to the circleInProcess for each new event up to two events. Two events removes the linesInProcess, which really means we should change the name of the dictionary to lineInProcess, and three line events reverts the whole thing to just one line - the third touch.

[code]
// Put pair in dictionary
[linesInProcess setObject:newLine forKey:key];

    [circleInProcess setObject:newLine forKey:key];

    if ([linesInProcess count] == 2)
        [linesInProcess removeAllObjects];[/code]

Then in touchesMoved:withEvent: the circle also gets updated whether or not the user intends to make a circle, just in case a second finger comes down.

[code]- (void)touchesMoved:(NSSet *)touches
withEvent:(UIEvent *)event
{

// Update linesInProcess and circleInProcess with moved touches
for (UITouch *t in touches) {
    NSValue *key = [NSValue valueWithNonretainedObject:t];
    

    // Find the line for this touch
    Line *line = [linesInProcess objectForKey:key];
    Line *circle = [circleInProcess objectForKey:key];
    CGPoint loc = [t locationInView:self];

    // Update the line
    if (line)
        [line setEnd:loc];
    
 // Update the circle    
    if (circle)
        [circle setEnd:loc];
                
}
// Redraw
[self setNeedsDisplay];

}[/code]

Finally, endTouches: is where all the magic comes in. It’s important to note that finishing a line needs to clear out any half-started circleInProcess objects.

[code]- (void)endTouches:(NSSet *)touches
{
// Remove ending touches from dictionary
for (UITouch *t in touches) {

    NSValue *key = [NSValue valueWithNonretainedObject:t];
    
    Line *line = [linesInProcess objectForKey:key];
    Line *circle = [circleInProcess 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];
        [circleInProcess removeAllObjects];
    }
    
    // If a circle has been started, removing one or two fingers sets the circle in place
    if ([circleInProcess count] == 2) {
        for (NSValue *v in circleInProcess) {
            circle = [circleInProcess objectForKey:v];
            [completeCircles addObject:circle];
        }
        [circleInProcess removeAllObjects];
    }
}
// Redraw
[self setNeedsDisplay];

}[/code]

Some of the drawbacks to this code include its inability to cope with three or more lines at a time any longer, which is no great loss. However, it might be nice if the circle drawing reverted to a line again if both fingers aren’t removed near-simultaneously. As the code is now, removing one finger while drawing a circle permanently sets the circle. Another design addition would be to make the drawing of circles and lines appear in the order the user put them down. So, if I draw a circle, a line, a line, and then another circle, they should appear in that order; right now circles appear on top. So, while this isn’t the most elegant solution, it works!


#2

Hi,

I did things a bit differently … indeed, I didn’t want to mix lines and circles, so I created a new class called Circle and, in order to archive circles, a CircleStore.

Circle.h:

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

@interface Circle : NSObject

@property (nonatomic) CGPoint center;
@property (nonatomic) float radius;
@property (nonatomic) UIColor *colorValue;

@end
[/code]

Circle.m:

[code]#import “Circle.h”

@implementation Circle

@synthesize center, radius;
@synthesize colorValue;

  • (void)encodeWithCoder:(NSCoder *)aCoder
    {
    [aCoder encodeCGPoint:center forKey:@“center”];
    [aCoder encodeFloat:radius forKey:@“radius”];
    [aCoder encodeObject:colorValue forKey:@“colorValue”];

}

  • (id)initWithCoder:(NSCoder *)aDecoder
    {
    self = [super init];
    if (self) {
    [self setCenter:[aDecoder decodeCGPointForKey:@“center”]];
    [self setRadius:[aDecoder decodeFloatForKey:@“radius”]];
    [self setColorValue:[aDecoder decodeObjectForKey:@“colorValue”]];
    }
    return self;
    }

@end
[/code]

CircleStore.h:

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

@class Circle;

@interface CircleStore : NSObject
{
NSMutableArray *allCircles;
}

  • (CircleStore *)sharedStore;
  • (NSMutableArray *)allTheCircles;

  • (NSString *)circleArchivePath;

  • (BOOL)saveChanges;

@end
[/code]

CircleStore.m:

[code]#import “CircleStore.h”
#import “Circle.h”

@implementation CircleStore

  • (CircleStore *)sharedStore
    {
    static CircleStore *sharedStore = nil;
    if (!sharedStore) {
    sharedStore = [[super allocWithZone:nil] init];
    }

    return sharedStore;
    }

  • (id)allocWithZone:(NSZone *)zone
    {
    return [self sharedStore];
    }

  • (id)init
    {
    self = [super init];
    if (self) {

      NSString *path = [self circleArchivePath];
      allCircles = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    
      if (!allCircles)
          allCircles = [[NSMutableArray alloc]init];
    

    }
    return self;
    }

  • (NSMutableArray *)allTheCircles
    {
    return allCircles;
    }

  • (NSString *)circleArchivePath
    {
    NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

    // Get one and only document directory from that list
    NSString *documentDirectory = [documentDirectories objectAtIndex:0];

    return [documentDirectory stringByAppendingPathComponent:@“circles.archive”];
    }

  • (BOOL)saveChanges
    {
    // return path success or failure
    NSString *path = [self circleArchivePath];
    return [NSKeyedArchiver archiveRootObject:allCircles
    toFile:path];
    }

@end
[/code]

Then I worked on the TouchDrawView to make things happen.

In TouchDrawView.h, I declared a new NSMutable Dictionary for circles in process, and a new NSMutableArray for complete circles.

[code]#include <math.h>

#import <Foundation/Foundation.h>

@interface TouchDrawView : UIView
{
NSMutableDictionary *linesInProcess;
NSMutableArray *completeLines;

NSMutableDictionary *circlesInProcess;
NSMutableArray *completeCircles;

}

  • (void)clearAll;
  • (void)endTouches:(NSSet *)touches;

@end[/code]

Then for TouchDrawView.m.

initWithFrame: works the same as for lines.
clearAll: works the same as for lines.

Here is DrawRect:

[code] // Draw complete circles
for (Circle *circle in completeCircles) {
// Color of the circle is random
if (![circle colorValue]) {
float hue = (rand() % 101 / 100.0);
NSLog(@"Hue: %.4f ", hue);
[circle setColorValue:[UIColor colorWithHue:hue saturation:1.0 brightness:1.0 alpha:1.0]];
}
[[circle colorValue]set];

    CGContextAddArc(context, [circle center].x, [circle center].y, [circle radius], 0, M_PI * 2.0, 1);
    CGContextStrokePath(context);
}    


// Draw circles in process
[[UIColor redColor]set];
for (NSValue *v in circlesInProcess) {
    Circle *circle = [circlesInProcess objectForKey:v];
    CGContextAddArc(context, [circle center].x, [circle center].y, [circle radius], 0, M_PI * 2.0, 1);
    CGContextStrokePath(context);
}

[/code]
Just for fun, I made the color of the circle random …

And for the multi-touch management:

The main point is that I didn’t manage one touch at a time as for lines. So there is no “for” loop when 2 touches are detected.
I need to define a box using both touches at the same time.

To draw the circle inside a box, I defined the center of the circle as the middle of the segment joining the 2 points.
And the radius is therefore the minimal distance from the center to the edge of the rectangle, therefore min(delta x, delta y).
Too bad I can’t post an image, but when you draw it on a paper, it really is obvious.

For the last part (endTouches:), I simply checked if a circle was in process. Because I can have only one circle in process at a time.
So if there is one, I finish it, and if not, I consider I’m drawing lines …

[code]- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// GOLD Challenge - drawing circles with 2 fingers
// Multi-touch with 2 fingers exactly
if ([touches count] == 2) {

    NSArray *points = [touches allObjects];
    // Identify a circle with the NSValue of the first touch
    NSValue *key = [NSValue valueWithNonretainedObject:[points objectAtIndex:0]];
    CGPoint point1 = [[points objectAtIndex:0] locationInView:self];
    CGPoint point2 = [[points objectAtIndex:1] locationInView:self];
    
    // Center of the circle is the center of the segment between the 2 points
    CGPoint centerPoint = CGPointMake((point1.x + point2.x) / 2.0, (point1.y + point2.y) / 2.0);
    // Radius of the circle is the minimum between delta x and delta y
    float radiusLength = MIN(fabsf((point2.x - point1.x) / 2.0) , fabsf((point2.y - point1.y) / 2.0));
    
    Circle *newCircle = [[Circle alloc]init];
    [newCircle setCenter:centerPoint];
    [newCircle setRadius:radiusLength];
    
    // Put pair in dictionary
    [circlesInProcess setObject:newCircle forKey:key];
    
} else {
// Other cases        
    for (UITouch *t in touches) {
        
        // Is this a double-tap ?
        if ([t tapCount] > 1) {
            [self clearAll];
            return;
        }
        
        // Use the touch object (packed in a 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];
        
        // Put pair in dictionary
        [linesInProcess setObject:newLine forKey:key];
    }
}

}

  • (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
    {

    // Multi-touch with 2 fingers exactly
    if ([touches count] == 2) {

      NSArray *points = [touches allObjects];
      NSValue *key = [NSValue valueWithNonretainedObject:[points objectAtIndex:0]];
      CGPoint point1 = [[points objectAtIndex:0] locationInView:self];
      CGPoint point2 = [[points objectAtIndex:1] locationInView:self];
      
      CGPoint centerPoint = CGPointMake((point1.x + point2.x) / 2.0, (point1.y + point2.y) / 2.0);
      float radiusLength = fabsf((point2.y - point1.y) / 2.0);
      
      Circle *existingCircle = [circlesInProcess objectForKey:key];
      [existingCircle setCenter:centerPoint];
      [existingCircle setRadius:radiusLength];
    

    } else {

      // Update linesInProcess with moved touches
      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];
      }
    

    }
    // Redraw
    [self setNeedsDisplay];
    }

  • (void)endTouches:(NSSet *)touches
    {
    // If a circle is being drawn
    if ([circlesInProcess count]) {

      NSArray *points = [touches allObjects];
      NSValue *key = [NSValue valueWithNonretainedObject:[points objectAtIndex:0]];
      Circle *existingCircle = [circlesInProcess objectForKey:key];
      if (existingCircle) {
          [completeCircles addObject:existingCircle];
          [circlesInProcess removeAllObjects]; // There can be only one circle in process at a time
          [linesInProcess removeAllObjects];
      }
    

    } else {
    // If no circle is being drawn, let’s consider lines in process

      // 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];
              [circlesInProcess removeAllObjects];
          }
      }
    

    }

    // Redraw
    [self setNeedsDisplay];
    }

[/code]

That works really well on the simulator, and quite well on the iPhone, although the most delicate part is to really use 2 fingers at the same time.
I wonder if there is a way to adjust the timing between 2 touches in order to consider it is a 2-finger gesture or 2 distinct gestures.

Have fun !
Fred


#3

I am having the strangest problem. For some reason my completeCircles always come out red. I’m not sure why but even doing [[UIColor grayColor] set] doesn’t set them to gray. The only way I can get them to be a different color is to comment out the [[UIColor redColor] set] line that is just before looping through the circlesInProcess. Did the way to set the color before calling CGContextStrokePath change or something?

UPDATE: I figured it out. I forgot to clear out circlesInProcess within endTouches.
Thanks,
Mark