A line is defined by two points. Your BNRLine stores these points as properties named begin and end. When a touch begins, you will create a line and set both begin and end to the point where the touch began. When the touch moves, you will update end. When the touch ends, you will have your complete line.
In BNRDrawView.m, implement touchesBegan:withEvent: to create a new line.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *t = [touches anyObject]; // Get location of the touch in view's coordinate system CGPoint location = [t locationInView:self]; self.currentLine = [[BNRLine alloc] init]; self.currentLine.begin = location; self.currentLine.end = location; [self setNeedsDisplay]; }
Then, in BNRDrawView.m, implement touchesMoved:withEvent: so that it updates the end of the currentLine.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *t = [touches anyObject]; CGPoint location = [t locationInView:self]; self.currentLine.end = location; [self setNeedsDisplay]; }
Finally, in BNRDrawView.m, add the currentLine to the finishedLines when the touch ends.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self.finishedLines addObject:self.currentLine]; self.currentLine = nil; [self setNeedsDisplay]; }
Build and run the application and draw some lines on the screen. While you are drawing, the lines will appear in red and once finished, they will appear in black.
When drawing lines, you may have noticed that having more than one finger on the screen does not do anything – that is, you can only draw one line at a time. Let’s update BNRDrawView so that you can draw as many lines as you can fit fingers on the screen.
By default, a view will only accept one touch at a time. If one finger has already triggered touchesBegan:withEvent: but has not finished – and therefore has not triggered touchesEnded:withEvent: – subsequent touches are ignored. In this context, “ignore” means that the BNRDrawView will not be sent touchesBegan:withEvent: or any other UIResponder messages related to the extra touches.
In BNRDrawView.m, enable BNRDrawView instances to accept multiple touches.
- (instancetype)initWithFrame:(CGRect)r { self = [super initWithFrame:r]; if (self) { self.finishedLines = [[NSMutableArray alloc] init]; self.backgroundColor = [UIColor grayColor]; self.multipleTouchEnabled = YES; } return self; }
Now that BNRDrawView will accept multiple touches, each time a finger touches the screen, moves, or is removed from the screen, the view will receive the appropriate UIResponder message. However, this now presents a problem: your UIResponder code assumes there will only be one touch active and one line being drawn at a time.
Notice, first, that each touch handling method you have already implemented sends the message anyObject to the NSSet of touches it receives. In a single-touch view, there will only ever be one object in the set, so asking for any object will always give you the touch that triggered the event. In a multiple touch view, that set could contain more than one touch.
Then, notice that there is only one property (currentLine) that hangs on to a line in progress. Obviously, you will need to hold as many lines as there are touches currently on the screen. While you could create a few more properties, like currentLine1 and currentLine2, you would have to go to considerable lengths to manage which instance variable corresponds to which touch.
Instead of the multiple property approach, you can use an NSMutableDictionary to hang on to each BNRLine in progress. The key to store the line in the dictionary will be derived from the UITouch object that the line corresponds to. As more touch events occur, you can use the same algorithm to derive the key from the UITouch that triggered the event and use it to look up the appropriate BNRLine in the dictionary.
In BNRDrawView.m, add a new instance variable to replace the currentLine and instantiate it in initWithFrame:.
@interface BNRDrawView ()@property (nonatomic, strong) BNRLine *currentLine;@property (nonatomic, strong) NSMutableDictionary *linesInProgress; @property (nonatomic, strong) NSMutableArray *finishedLines; @end @implementation BNRDrawView - (instancetype)initWithFrame:(CGRect)r { self = [super initWithFrame:r]; if (self) { self.linesInProgress = [[NSMutableDictionary alloc] init]; self.finishedLines = [[NSMutableArray alloc] init]; self.backgroundColor = [UIColor grayColor]; self.multipleTouchEnabled = YES; } return self; }
Now you need to update the UIResponder methods to add lines that are currently being drawn to this dictionary. In BNRDrawView.m, update the code in touchesBegan:withEvent:.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // Let's put in a log statement to see the order of events 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; } UITouch *t = [touches anyObject]; CGPoint location = [t locationInView:self]; self.currentLine = [[BNRLine alloc] init]; self.currentLine.begin = location; self.currentLine.end = location; [self setNeedsDisplay]; }
First, notice that you use fast enumeration to loop over all of the touches that began, because it is possible that more than one touch can begin at the same time. (Although typically touches begin at different times and BNRDrawView will receive multiple touchesBegan:withEvent: messages containing each touch.)
Next, notice the use of valueWithNonretainedObject: to derive the key to store the BNRLine. This method creates an NSValue instance that holds on to the address of the UITouch object that will be associated with this line. Since a UITouch is created when a touch begins, updated throughout its lifetime, and destroyed when the touch ends, the address of that object will be constant through each touch event message.
Update touchesMoved:withEvent: in BNRDrawView.m so that it can look up the right BNRLine.
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { // Let's put in a log statement to see the order of events NSLog(@"%@", NSStringFromSelector(_cmd)); for (UITouch *t in touches) { NSValue *key = [NSValue valueWithNonretainedObject:t]; BNRLine *line = self.linesInProgress[key]; line.end = [t locationInView:self]; } UITouch *t = [touches anyObject]; CGPoint location = [t locationInView:self]; self.currentLine.end = location; [self setNeedsDisplay]; }
Then, update touchesEnded:withEvent: to move any finished lines into the _finishedLines array.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { // Let's put in a log statement to see the order of events 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.finishedLines addObject:self.currentLine]; self.currentLine = nil; [self setNeedsDisplay]; }
Finally, update drawRect: to draw each line in _linesInProgress.
// Draw finished lines in black [[UIColor blackColor] set]; for (BNRLine *line in self.finishedLines) { [self strokeLine:line]; } [[UIColor redColor] set]; for (NSValue *key in self.linesInProgress) { [self strokeLine:self.linesInProgress[key]]; } if (self.currentLine) { // Draw line in progress in red [[UIColor redColor] set]; [self strokeLine:self.currentLine]; }}
Build and run the application and start drawing lines with multiple fingers. (You can simulate multiple fingers on the simulator by holding down the option key as you drag.)
You may be wondering: why not use the UITouch itself as the key? Why go through the hoop of creating an NSValue? Objects used as keys in an NSDictionary must conform to the NSCopying protocol, which allows them to be copied by sending the message copy. UITouch instances do not conform to this protocol because it does not make sense for them to be copied. Thus, the NSValue instances hold the address of the UITouch so that equal NSValue instances can be later created with the same UITouch.
Also, you should know that when a UIResponder message like touchesMoved:withEvent: is sent to a view, only the touches that have moved will be in the NSSet of touches. Thus, it is possible for three touches to be on a view, but only one touch inside the set of touches passed into one of these methods if the other two did not move. Additionally, once a UITouch begins on a view, all touch event messages are sent to that same view over the touch’s lifetime, even if that touch moves off of the view it began on.
The last thing left for the basics of TouchTracker is to handle what happens when a touch is cancelled. A touch can be cancelled when an application is interrupted by the operating system (for example, a phone call comes in) when a touch is currently on the screen. When a touch is cancelled, any state it set up should be reverted. In this case, you should remove any lines in progress.
In BNRDrawView.m, implement touchesCancelled:withEvent:.
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { // Let's put in a log statement to see the order of events NSLog(@"%@", NSStringFromSelector(_cmd)); for (UITouch *t in touches) { NSValue *key = [NSValue valueWithNonretainedObject:t]; [self.linesInProgress removeObjectForKey:key]; } [self setNeedsDisplay]; }