Wow


#1

I’ve spent about three days trying to figure this challenge out, and I’ve come across a couple of ways that DON’T work:

The snag usually revolved around multiple touches: how do you know which touch goes with which circle?

Things that didn’t work included (a) tracking the number of fingers on the screen (with an integer called touchCount) and then, depending if it’s even or odd, do something funky with the points you’re working with: either set/reset the center point or adjust the radius), and (b) use touchCount again, and, if we’re drawing circles, set begin/end points of a line in linesInProcess but use that as the bounding points of a circle in the drawRect method.

In all cases, I get bogged down because in the case of the circle, you’re tracking two distinct touch points to set boundaries for the circle you’re working with. It gets even more complex if you have three (or four, or five) fingers on the screen at once.

But, I think I’ve got it figured out, and the key, I think, (I’ve yet to try coding this solution) is to approach the circles from completely different “points” of view.

I’ll post a follow-up in this thread to let you all know if I got it working (or am trying something else).


#2

I’d suggest having an array of dictionaries to hold onto the touches.

Keep the standard linesInProcess. When a touch occurs, add it to the linesInProcess as normal. When a second touch joins the screen, create a new dictionary with the just-added touch and the previous touch (removing it from linesInProcess), add this dictionary to the array. If a third touch joins the screen, add it to linesInProcess, when the fourth hits, do the same as you did for the 2nd touch (with a new dictionary).

When a touch ends, if it is a lineInProcess, add to completeLines. If both touches in a circleDict end, add that circle to the completeCircles array. Shouldn’t be more than 15 or so extra lines of code, drawing included.


#3

OMG, the revelation has just hit me, and I feel like I’ve had my brain smashed out by a lump of lemon wrapped around a large, gold brick.

The touches set that is passed to the methods touchesBegan, touchesMoved, touchesEnded, and touchesCancelled (the last two methods both simply call ours named endTouches) aren’t a set of ALL the touches on the screen, just the touches that have updated (started, moved, or ended.)

Thus, the implementation I was trying (which I’d started before Joe replied and made me stop and rethink) won’t work.

What I was going to do (and, with some tweaking, it still might work) was create a dictionary of points representing the starting points and endpoints of the circles, then, when an event happened, iterate through the touches and manipulate the points accordingly – except that would only work if the touches set that was passed to each method was a complete set of touches.

This approach would work fine for touchesMoved and touchesBegan, which initiate the drawing process, however it won’t work for the touchesEnded method (it would, however, work for touchesCancelled as the touches set would be everything at that point). The problem you run into is what if you only lift one finger from a circle (or don’t lift both fingers quickly enough for both to be in the touches set when touchesEnded is called?) You could pop the point you’ve released into a completed circles dictionary (or array) just fine, but, unless you have a way to tell it what touch it’s supposed to associate with (and you won’t), you’re hooped.

Thanks, Joe. You’ve saved me a lot of time.


#4

After a week’s vacation, I’ve come back to this challenge…

Here’s where I’m stuck (yes, I’m still stuck on this challenge).

Circle.h (so you have an idea what I’m doing)

#import <Foundation/Foundation.h>

@class Line;
@interface Circle : NSObject <NSCoding> {

	NSMutableDictionary *points;
}
@property (nonatomic, retain) NSMutableDictionary *points;
@end

First touch on the screen, point gets put into a Line object, and put into linesInProcess no problem.
Second touch on the screen:
1: initial point gets pulled out of linesInProcess (using an iVar called oldKey to grab it) (call it points)
2: second point gets put into a Line object (for simplicity’s sake and because I got errors when I tried putting a CGPoint into an array or dictionary).
3: first point gets put into a local dictionary with setObject, forKey:oldKey
4: second point gets put into a local dictionary with setObject, forKey:key
5: local dictionary is sent to a Circle object (called newCircle) with [newCircle setPoints:] message.
6: newCircle is then added to an array called incompleteCircles for storage.
7: the local dictionary, newCircle, oldPoint and newLine are released.

(This is a variant of Joe’s suggestion to set up an array of dictionaries to hold onto the touches, but while this implementation may take a little more code, the methodology of implementing it is similar)

All that seems to work just fine. When I trace through the program using the debugger, I can see the information going where it’s supposed to go, and all is stored just fine.

The problem I’m running into is in the touchesMoved and endTouches methods, where now we need to go back and retrieve the circle to edit/finalize based on the touch that ended/moved.

In touchesMoved, I set up the following:

		NSValue *key = [NSValue valueWithPointer:t];

// Other code in here for dealing with the case of the user drawing lines; left out because it works just fine.

			for (Circle *circle in incompleteCircles) { 
				NSDictionary *pts = [circle points];
				for (NSValue *k in pts) {
					if (k == key) {
						Line *line = [circle.points objectForKey:key]; // Get a pointer to the appropriate point
						[line setBegin:loc];				          
						[line setEnd:loc];
					}
				}

When I trace through this, key never equals k despite that it works when pulling data out of linesInProcess above.

So, what am I missing here? When I trace the program, I see that key is assigned a value that is totally different than anything used earlier.

(Note - I’m also not convinced that even when I finally do manage to find the appropriate key that the line of code that includes [circle.points objectForKey:key]; will work. I think it may need to be changed to [[circle points] objectForKey:key]; )

Help?


#5

Below is my solution. Something to note is that there is a big difference between solving this challenge for the simulator and doing so for the device.

On the device when the fingers are dragged across the screen, sometimes the iPhone detects only one finger. This is why you will some extra “insurance” checks here. Also, please note that there may be some code I may not need anymore that is here as part of the troubleshooting process for the problem described previously.

Circle.h

#import <Foundation/Foundation.h>

@interface Circle : NSObject {}
@property (nonatomic) CGPoint start;
@property (nonatomic) CGPoint end;

@end

Circle.m

#import "Circle.h"

@implementation Circle
@synthesize start, end;
@end

TouchDrawView.h

#import <UIKit/UIKit.h>


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

	NSMutableDictionary *circlesInProcess;
	NSMutableArray      *completeCircles;
}

@end

TouchDrawView.m

#import "TouchDrawView.h"
#import "Line.h"
#import "Circle.h"

// TODO: Make the app more reliable on the device by locking a "circle mode"
// or "line mode" after the touchesBegan is engaged
// TODO: Shake to undo? That would be neat!

@implementation TouchDrawView

#pragma mark -
#pragma mark Initialization and Memory Management
- (id)initWithCoder:(NSCoder *)c {
	[super initWithCoder:c];
	linesInProcess   = [[NSMutableDictionary alloc] init];
	completeLines    = [[NSMutableArray alloc] init];
	circlesInProcess = [[NSMutableDictionary alloc] init];
	completeCircles  = [[NSMutableArray alloc] init];
	[self setMultipleTouchEnabled:YES];

	return self;
}

- (void)dealloc {
	[linesInProcess release];
	[completeLines release];
	[circlesInProcess release];
	[linesInProcess release];
	[super dealloc];
}

#pragma mark -
#pragma mark Drawing
- (void)drawRect:(CGRect)rect {
	CGContextRef context = UIGraphicsGetCurrentContext();

	CGContextSetLineWidth(context, 10.0);
	CGContextSetLineCap(context, kCGLineCapRound);

	// Draw lines in black
	[[UIColor blackColor] set];

	for (Line *line in completeLines) {
		CGContextMoveToPoint(context, [line begin].x, [line begin].y);
		CGContextAddLineToPoint(context, [line end].x, [line end].y);
		CGContextStrokePath(context);
	}

	// Draw lines in process in red
	[[UIColor redColor] set];

	for (NSValue *v in linesInProcess) {
		Line *line = [linesInProcess objectForKey:v];
		CGContextMoveToPoint(context, [line begin].x, [line begin].y);
		CGContextAddLineToPoint(context, [line end].x, [line end].y);
		CGContextStrokePath(context);
	}

	// CHALLENGE
	// Draw circles in black
	[[UIColor blackColor] set];

	for (Circle *circle in completeCircles) {
		CGContextMoveToPoint(context, [circle end].x, [circle end].y);
		CGContextAddEllipseInRect(context, CGRectMake([circle end].x
											, [circle end].y
											, [circle start].x - [circle end].x
											, [circle start].y - [circle end].y));
		CGContextStrokePath(context);
	}

	// Draw circles in process in red
	[[UIColor redColor] set];

	for (NSValue *v in circlesInProcess) {
		Circle *circle = [circlesInProcess objectForKey:v];
		CGContextMoveToPoint(context, [circle end].x, [circle end].y);
		CGContextAddEllipseInRect(context, CGRectMake([circle end].x
											, [circle end].y
											, [circle start].x - [circle end].x
											, [circle start].y - [circle end].y));
		CGContextStrokePath(context);
	}
}

- (void)clearAll {
	// Clear the containers
	[linesInProcess removeAllObjects];
	[completeLines removeAllObjects];
	[circlesInProcess removeAllObjects];
	[completeCircles removeAllObjects];

	// Redraw
	[self setNeedsDisplay];
}

#pragma mark -
#pragma mark Touches

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
	NSLog(@"touchesBegan, #Touches: %i", [touches count]);
	switch ([touches count]) {
		case 1: // Draw a line
		{
			[linesInProcess removeAllObjects];

			// Clear the canvas
			for (UITouch *t in touches) {
				// Is this a double tap?
				if ([t tapCount] > 1) {
					[self clearAll];
					return;
				}

				// Use the touch object (packed in an NSValue) as the key
				NSValue *key = [NSValue valueWithPointer:t]; // we need to this because
				                                             // UITouch doesn't implement
				                                             // NSCoding

				// 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];
				NSLog(@"lines: %i ", [linesInProcess count]);
				[newLine release];
			}

			break;
		}

		case 2:
		{
			// CHALLENGE: Make a circle
			// Detect two fingers to make a circle
			[circlesInProcess removeAllObjects];
			// Create a temporary arrary to iterate over the members of the touches set
			NSMutableArray *a = [NSMutableArray arrayWithObjects:nil];

			for (UITouch *t in touches) {
				[a addObject:t];
			}

			// Create dictionary key (UITouch doesn't implement NSCoding
			NSValue *key = [NSValue valueWithPointer:[a objectAtIndex:0]];

			// Create a circle instance
			CGPoint pa        = [[a objectAtIndex:0] locationInView:self];
			CGPoint pb        = [[a objectAtIndex:1] locationInView:self];
			int     minx      = MIN(pa.x, pb.x);
			int     maxx      = MAX(pa.x, pb.x);
			int     miny      = MIN(pa.y, pb.y);
			int     maxy      = MAX(pa.y, pb.y);
			CGPoint start     = CGPointMake(minx, miny);
			CGPoint end       = CGPointMake(maxx, maxy);
			Circle *newCircle = [[Circle alloc] init];
			[newCircle setStart:start];
			[newCircle setEnd:end];

			// Put circle in dictionary
			[circlesInProcess setObject:newCircle forKey:key];
			[newCircle release];

			break;
		}
	}
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
	NSLog(@"touchesMoved, #Touches: %i", [touches count]);

	switch ([touches count]) {
		case 1: // Draw a line
		{
			if ([linesInProcess count] > 0) {
				// Update linesInProcess with moved touches
				for (UITouch *t in touches) {
					// Use the touch object (packed in an NSValue) as the key
					NSValue *key = [NSValue valueWithPointer:t]; // we need to this because
					                                             // UITouch doesn't implement
					                                             // NSCoding

					// Find the line for this touch
					Line *line = [linesInProcess objectForKey:key];

					// Update the line
					CGPoint loc = [t locationInView:self];
					[line setEnd:loc];
				}
			}

			break;
		}

		case 2: // Draw a circle
		{
			if ([circlesInProcess count] > 0) {
				// CHALLENGE: Make a circle
				// Detect two fingers to make a circle

				// Create a temporary arrary to iterate over the members of the touches set
				NSMutableArray *a = [NSMutableArray arrayWithObjects:nil];

				for (UITouch *t in touches) {
					[a addObject:t];
				}

				// Get the temporary circle from the dictionary
				NSValue *key = [NSValue valueWithPointer:nil];

				// There is only one element in the Dictionary so we can safely extract it
				// with this loop
				for (NSValue *v in circlesInProcess) {
					key = v;
				}

				// Update the circle
				Circle *circle = [circlesInProcess objectForKey:key];
				CGPoint pa     = [[a objectAtIndex:0] locationInView:self];
				CGPoint pb     = [[a objectAtIndex:1] locationInView:self];
				int     minx   = MIN(pa.x, pb.x);
				int     maxx   = MAX(pa.x, pb.x);
				int     miny   = MIN(pa.y, pb.y);
				int     maxy   = MAX(pa.y, pb.y);
				CGPoint start  = CGPointMake(minx, miny);
				CGPoint end    = CGPointMake(maxx, maxy);
				[circle setStart:start];
				[circle setEnd:end];

				// Update circle in dictionary
				[circlesInProcess setObject:circle forKey:key];
			}

			break;
		}
	}

	// Redraw
	[self setNeedsDisplay];
}

- (void)endTouches:(NSSet *)touches {
	NSLog(@"endTouches, #Touches: %i", [touches count]);

	if ([linesInProcess count] > 0) {
		// It's a line
		// Remove ending touches from dictionary
		for (UITouch *t in touches) {
			NSValue *key  = [NSValue valueWithPointer:t];
			Line    *line = [linesInProcess objectForKey:key];

			// If this is a double tap, 'line' will be nil
			if (line) {
				[completeLines addObject:line];
				[linesInProcess removeObjectForKey:key];
			}
		}
	}

	if ([circlesInProcess count] > 0) {
		NSLog(@"Ending Circle");
		// CHALLENGE: Make a circle

		// Get the temporary circle from the dictionary
		// Circle *circle = [[Circle alloc] init];
		NSValue *key = [NSValue valueWithPointer:nil];

		// There is only one element in the Dictionary so we can safely extract it
		// with this loop
		for (NSValue *v in circlesInProcess) {
			key = v;
		}

		Circle *circle = [circlesInProcess objectForKey:key];

		if (circle) {
			[completeCircles addObject:circle];
			[circlesInProcess removeObjectForKey:key];
		}
	}

	// Insurance: Remove all objects (sometimes the touches on the device are not
	// super reliable)
	[circlesInProcess removeAllObjects];
	[linesInProcess removeAllObjects];

	// Redraw
	[self setNeedsDisplay];
}

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

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

@end

Good luck!


#6

[quote]I’d suggest having an array of dictionaries to hold onto the touches.

Keep the standard linesInProcess. When a touch occurs, add it to the linesInProcess as normal. When a second touch joins the screen, create a new dictionary with the just-added touch and the previous touch (removing it from linesInProcess), add this dictionary to the array. If a third touch joins the screen, add it to linesInProcess, when the fourth hits, do the same as you did for the 2nd touch (with a new dictionary).

When a touch ends, if it is a lineInProcess, add to completeLines. If both touches in a circleDict end, add that circle to the completeCircles array. Shouldn’t be more than 15 or so extra lines of code, drawing included.[/quote]

I just started working through the book a couple of weeks ago so I thought I’d add my 2c worth. I was determined to do this challenge this way and it didn’t come to much more than 15 lines of code but it took me a day to get them. These five in touchesBegan probably gave me the most grief.

if ([linesInProcess count] == 2) { NSMutableDictionary *doubleLinesInProcess = [[NSMutableDictionary alloc] init]; [doubleLinesInProcess addEntriesFromDictionary:linesInProcess]; [linesInProcess removeAllObjects]; [circlesInProcess addObject:doubleLinesInProcess]; }
I understand better how the touches work now though so I reckon it’s a pretty good challenge.