Bronze, Silver and Gold challenge solutions


#1

Bronze:
In initWithFrame: in TouchDrawView.m:

UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)]; [doubleTapRecognizer setNumberOfTapsRequired:2]; [self addGestureRecognizer:doubleTapRecognizer];

In TouchDrawView.m:

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

Silver:
In touchesBegan:withEvent: in TouchDrawView.m:

if([self selectedLine]) { [[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES]; [self setSelectedLine:nil]; }

Gold:
In Line.h:

In Line.m:

In TouchDrawView.m:

-(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];
		
		/**************************
		* Gold Challenge - Determine line thickness based on drawing velocity
		**************************/
		CGPoint drawVelocity = [moveRecognizer velocityInView:self];
		drawVelocity.x = fabsf(drawVelocity.x); // Convert negative float value to positive
		drawVelocity.y = fabsf(drawVelocity.y); // Convert negative float value to positive
		
		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];
}

-(void)drawRect:(CGRect)rect {
	CGContextRef context = UIGraphicsGetCurrentContext();
	// CGContextSetLineWidth(context, 10.0); Commented out this line
	CGContextSetLineCap(context, kCGLineCapRound);
	
	// Draw complete lines in black
	[[UIColor blackColor] set];
	for(Line *line in completeLines) {
		CGContextSetLineWidth(context, [line thickness]); // Gold challenge
		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
	[[UIColor redColor] set];
	for(NSValue *v in linesInProcess) {
		Line *line = [linesInProcess objectForKey:v];
		
		CGContextSetLineWidth(context, [line thickness]); // Gold challenge
		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]); // Gold challenge
		CGContextMoveToPoint(context, [[self selectedLine] begin].x, [[self selectedLine] begin].y);
		CGContextAddLineToPoint(context, [[self selectedLine] end].x, [[self selectedLine] end].y);
		CGContextStrokePath(context);
	}
}

I’m pretty sure there’s a better way to determine line thickness based on velocity, but this is the best I could come up with. For the gold challenge, I just pasted in all the modified methods because there’s a few lines of code spread throughout them in regards to the challenge. I’ve added comments on the lines in question. It works, but I’d love to see some other approaches to this challenge :slight_smile:


#2

I did the same thing for gold but I did it within moveLine: instead of touchesMoved:withEvent:

if (![self selectedLine]) { float x = abs([gr velocityInView:self].x); float y = abs([gr velocityInView:self].y); for (NSValue *v in linesInProcess) { Line *line = [linesInProcess objectForKey:v]; [line setWidth:(x+y)/10]; } return; }

“width” is thickness in your version.


#3

for the silver challenge this is what I did in “touchesBegan” method:

[code]// If no lines are selected then begin creating new lines
if (![self selectedLine]) {
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];
        
        // Put pair in dictionary
        [linesInProcess setObject:newLine forKey:key];
    }

}[/code]
In effect, as long as a line is selected (i.e. selectedLine != nil), new lines will not be created and placed into the dictionary.

As for the gold challenge this is what I did:

In the class Line I added a property of type CGFloat called ‘width’ and then added three lines to the following method:

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

// Update linesInProcess with moved touches
for (UITouch *t in touches) {
          
    CGPoint velocity = [moveRecognizer velocityInView:self]; // <---------
    CGFloat lineWidth = hypotf(velocity.x, velocity.y)/50;   // <---------  
    
    NSValue *key = [NSValue valueWithNonretainedObject:t];
    
    // find the line for this touch
    Line *line = [linesInProcess objectForKey:key];
    [line setWidth:lineWidth];                          // <--------

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

}[/code]

The x and y velocity values from the move gesture recognizer broke down by swipe ‘vector’ into their x and y components. So in my code I used some vector math that I remembered from high school to represent the actual velocity of my motion (i.e. speed and direction). The hypo() function is in affect using the pythagorean theorem giving the hypothenuse from the x and y components. I then took that value and divided it by 50 (it was really a bit of trial an error coming up with that value).

I then accessed the Line.width property inside the drawRect method to assign it to my line width. This kept the line width for each line unique based on the drawing velocity.


#4

For the Gold challenge, in my drawRect: method, I set the line thickness as the context line width ONLY if this thickness is different from zero.
And only for complete and selected lines, not for lines in process.
This way, when I select a line, it’s still a green, 10-point wide line. Then when I move, the thicknes is adjusted to the velocity.

[code]- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
// CGContextSetLineWidth(context, 10.0);
CGContextSetLineCap(context, kCGLineCapRound);

// Draw complete lines in black
[[UIColor blackColor] set];
for (Line *line in completeLines) {
    if ([line thickness] != 0.0) {
        CGContextSetLineWidth(context, [line thickness]);
    } else {
        CGContextSetLineWidth(context, 10.0);
    }
    
    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];
    CGContextSetLineWidth(context, 10.0);
    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];
    if ([[self selectedLine] thickness] != 0.0) {
        CGContextSetLineWidth(context, [[self selectedLine] thickness]);
    } else {
        CGContextSetLineWidth(context, 10.0);
    }
    CGContextMoveToPoint(context, [[self selectedLine]begin].x , [[self selectedLine]begin].y );
    CGContextAddLineToPoint(context, [[self selectedLine]end].x, [[self selectedLine]end].y);
    CGContextStrokePath(context);
}

}
[/code]

And I calculated the pan velocity in moveLine:

[code]- (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 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];
    
    // GOLD Challenge - pan velocity
    CGPoint velocity = [gr velocityInView:self];
    NSLog(@"Pan Velocity: horizontal = %f - vertical = %f", velocity.x, velocity.y);
    [[self selectedLine] setThickness: hypot(velocity.x , velocity.y)/10];
    NSLog(@"thickness %f",[[self selectedLine]thickness]);
    
    // Redraw the screen
    [self setNeedsDisplay];
    
    [gr setTranslation:CGPointZero inView:self];
}

}[/code]


#5

For Silver, I approached this very human-logically.

What did I want to happen? Well, I want panning to keep watching the screen, but I want its move actions to only fire after a longPress is initiated I am holding a long press.

So, I created a moveRecognizerEnabled BOOL and set it to NO in init. Then I set it to YES in longPress. Then I set it back to no in endTouch.

This seemed like a simple way to accomplish what I was hoping to do - and it worked! :smiley:

-sean


#6

For the Silver Challenge, I wanted to clear lines in process whenever a line is being moved.

I added one line to moveLine in TouchDrawView.m:

[code]
// Set the new beginning and end points of the line
[[self selectedLine] setBegin:begin];
[[self selectedLine] setEnd:end];

   // SILVER: don't allow lines in process while moving a line
    [linesInProcess removeAllObjects];
    
    // Redraw the screen
    [self setNeedsDisplay];
    
    [gr setTranslation:CGPointZero inView:self];[/code]

Many thanks to TheEskil and billyshih for excellent Gold solutions.


#7

Although this didn’t turn out to work fully there where still small dots of black being created, I used the UIGestureRecogniser’s property CancelTouchesInView for the Silver challenge,

as so:

[code]- (void)moveLine:(UIPanGestureRecognizer *)gr
{
// If a line hasn’t been selected do nothing
if (![self selectedLine]) {
[gr setCancelsTouchesInView:NO];
return;
}

if ([gr state] == UIGestureRecognizerStateChanged) {
    CGPoint translation = [gr translationInView:self];
    
    
    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;
    
    [[self selectedLine] setBegin:begin];
    [[self selectedLine] setEnd:end];
    
    [self setNeedsDisplay];
    [gr setCancelsTouchesInView:YES];
}
[gr setTranslation:CGPointZero inView:self];

}
[/code]


#8

[quote=“TheEskil”]Bronze:
In initWithFrame: in TouchDrawView.m:

UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)]; [doubleTapRecognizer setNumberOfTapsRequired:2]; [self addGestureRecognizer:doubleTapRecognizer];

In TouchDrawView.m:

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

One loose end with this solution to Bronze: If you double-tap on or near a line, then the single-tap gesture recognizer will show the “Delete” menu, which will then keep floating even when all lines are cleared. It would be reasonable to explicitly hide the menu in either doubleTap: or clearAll, but then the state of tap: is spilling over into other methods, which rubs me the wrong way. Better, I think, is to add this line to initWithFrame::

  [tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];

This will introduce a perceptible delay between the actual tap and the single-tap gesture recognizer’s tap: message. But I still like it.


#9

For silver one I forbade to draw a new line while menu is up by using isMenuVisible property for touchesBegin method, like so:

if ([[UIMenuController sharedMenuController] isMenuVisible] == NO) { 
code for drawing a line
}