How to Effectively Process Uitouches in a Multitouch Sequence

How to ignore certain UITouch Points in multitouch sequence

One solution is to store the topmost tap in touchesBegan and only draw this one.

As you have pointed out, you are not supposed to retain the UITouch instance, so I recommend using a weak reference instead.

This will only draw a single touch. If you wish to draw the touches of multiple fingers, you need another way of filtering out the hand (many drawing apps have user settings for telling the app the pose of the hand, for example, but this is of course more complicated).

Here is an idea on how to do it:

#import <QuartzCore/QuartzCore.h>

@interface TViewController () {
// We store a weak reference to the current touch that is tracked
// for drawing.
__weak UITouch* drawingTouch;
// This is the previous point we drawed to, or the first point the user tapped.
CGPoint touchStartPoint;
}
@end
@interface _TDrawView : UIView {
@public
CGLayerRef persistentLayer, tempLayer;
}
-(void)commitDrawing;
-(void)discardDrawing;
@end

@implementation TViewController

- (void) loadView
{
self.view = [[_TDrawView alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.view.opaque = YES;
self.view.multipleTouchEnabled = YES;
self.view.backgroundColor = [UIColor whiteColor];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Start with what we currently have
UITouch* topmostTouch = self->drawingTouch;
// Find the top-most touch
for (UITouch *touch in touches) {
CGPoint lastPoint = [touch locationInView:self.view];
if(!topmostTouch || [topmostTouch locationInView:self.view].y > lastPoint.y) {
topmostTouch = touch;
touchStartPoint = lastPoint;
}
}
// A new finger became the drawing finger, discard any previous
// strokes since last touchesEnded
if(self->drawingTouch != nil && self->drawingTouch != topmostTouch) {
[(_TDrawView*)self.view discardDrawing];
}
self->drawingTouch = topmostTouch;
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
// Always commit the current stroke to the persistent layer if the user
// releases a finger. This could need some tweaking for optimal user experience.
self->drawingTouch = nil;
[(_TDrawView*)self.view commitDrawing];
[self.view setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
const CGFloat red=0, green=0, blue=0, brush=1;
for (UITouch *touch in touches) {
// Find the touch that we track for drawing
if(touch == self->drawingTouch) {
CGPoint currentPoint = [touch locationInView:self.view];

// Draw stroke first in temporary layer
CGContextRef ctx = CGLayerGetContext(((_TDrawView*)self.view)->tempLayer);
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, brush );
CGContextSetRGBStrokeColor(ctx, red, green, blue, 1.0);
CGContextSetBlendMode(ctx,kCGBlendModeNormal);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, touchStartPoint.x, touchStartPoint.y);
CGContextAddLineToPoint(ctx, currentPoint.x, currentPoint.y);
CGContextStrokePath(ctx);
// Update the points so that the next line segment is drawn from where
// we left off
touchStartPoint = currentPoint;
// repaint the layer
[self.view setNeedsDisplay];
}
}
}

@end

@implementation _TDrawView

- (void) finalize {
if(persistentLayer) CGLayerRelease(persistentLayer);
if(tempLayer) CGLayerRelease(tempLayer);
}

- (void) drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
if(!persistentLayer) persistentLayer = CGLayerCreateWithContext(ctx, self.bounds.size, nil);
if(!tempLayer) tempLayer = CGLayerCreateWithContext(ctx, self.bounds.size, nil);

// Draw the persistant drawing
CGContextDrawLayerAtPoint(ctx, CGPointMake(0, 0), persistentLayer);
// Overlay with the temporary drawing
CGContextDrawLayerAtPoint(ctx, CGPointMake(0, 0), tempLayer);
}

- (void)commitDrawing {
// Persist the temporary drawing
CGContextRef ctx = CGLayerGetContext(persistentLayer);
CGContextDrawLayerAtPoint(ctx, CGPointMake(0, 0), tempLayer);
[self discardDrawing];
}
- (void)discardDrawing {
// Clears the temporary layer
CGContextRef ctx = CGLayerGetContext(tempLayer);
CGContextClearRect(ctx, self.bounds);
CGContextFlush(ctx);
}
@end

EDIT: I added the logic that if a new touch is detected, if there is currently any stroke being drawn with a higher y-value, it is removed, as we discussed in the comments.

The overlaying is done by painting two CGLayers. This code could be optimized a lot for performance, it should be looked at more as an illustration than production-ready code.

Undo with multitouch drawing in iOS

m_redoArray appears to be the big daddy, the one you draw from. I don't understand why you empty this out out in 'touchesBegan...', surely one of these arrays must carry through touchesBegan unaltered, or you'll be dropping stuff from the start of your drawing all the way through, no?

It appears to me that this is how you dropped the 'Hell' in your example here..

How to track multiple touches

The touch object for each finger will stay the same with the same memory address while it's on the screen. You can keep track of individual fingers in a multi touch scenario by storing the address of touch objects in an array and then comparing against that array to know exactly which finger is moving.

var fingers = [String?](count:5, repeatedValue: nil)

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
for touch in touches{
let point = touch.locationInView(self.view)
for (index,finger) in fingers.enumerate() {
if finger == nil {
fingers[index] = String(format: "%p", touch)
print("finger \(index+1): x=\(point.x) , y=\(point.y)")
break
}
}
}
}

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesMoved(touches, withEvent: event)
for touch in touches {
let point = touch.locationInView(self.view)
for (index,finger) in fingers.enumerate() {
if let finger = finger where finger == String(format: "%p", touch) {
print("finger \(index+1): x=\(point.x) , y=\(point.y)")
break
}
}
}
}

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
for touch in touches {
for (index,finger) in fingers.enumerate() {
if let finger = finger where finger == String(format: "%p", touch) {
fingers[index] = nil
break
}
}
}
}

override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
super.touchesCancelled(touches, withEvent: event)
guard let touches = touches else {
return
}
touchesEnded(touches, withEvent: event)
}

Updated for swift 4

Credit to @Klowne

var fingers = [UITouch?](repeating: nil, count:5)

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
for touch in touches{
let point = touch.location(in: self.view)
for (index,finger) in fingers.enumerated() {
if finger == nil {
fingers[index] = touch
print("finger \(index+1): x=\(point.x) , y=\(point.y)")
break
}
}
}
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
for touch in touches {
let point = touch.location(in: self.view)
for (index,finger) in fingers.enumerated() {
if let finger = finger, finger == touch {
print("finger \(index+1): x=\(point.x) , y=\(point.y)")
break
}
}
}
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
for touch in touches {
for (index,finger) in fingers.enumerated() {
if let finger = finger, finger == touch {
fingers[index] = nil
break
}
}
}
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
guard let touches = touches else {
return
}
touchesEnded(touches, with: event)
}

*As per apple's updated documentation it's now OK to retain touches during a multi touch sequence as long as they are released at the end of the sequence

UITouch touchesMoved Finger Direction and Speed

Direction will be determined from the values of "location" and "prevLocation" in touchesMoved. Specifically, location will contain the new point of the touch. For example:

if (location.x - prevLocation.x > 0) {
//finger touch went right
} else {
//finger touch went left
}
if (location.y - prevLocation.y > 0) {
//finger touch went upwards
} else {
//finger touch went downwards
}

Now touchesMoved will get called many times for a given finger movement. It will be key to your code to compare an initial value when the finger first touches the screen, with a value of the CGPoint when the movement is finally complete.

Tracking Multiple Touches on iPhone

Your problem with them all needing to be touchdown/up at the same time is due to the fact that [touches allObjects] does not give you an array of all the fingers on the screen at any given time, it gives you an array of all the touches that match the current event (down/moved/ended/cancelled).

If you want to do it manually without using any gesturecognizers then you can get the full array of all touch objects by accessing event.allTouches. That will give you a full array and you can manage where touches are at all times.

Multitouch with UIPanGestureRecognizer

From the apple documentation for UIGestureRecogniser

(CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(UIView *)view
Parameters
touchIndex
The index of a UITouch object in a private array maintained by the receiver. This touch object represents a touch of the current gesture.
view
A UIView object on which the gesture took place. Specify nil to indicate the window.
Return Value
A point in the local coordinate system of view that identifies the location of the touch. If nil is specified for view, the method returns the touch location in the window’s base coordinate system.

(NSUInteger)numberOfTouches
Return Value
The number of UITouch objects in a private array maintained by the receiver. Each of these objects represents a touch in the current gesture.

Discussion
Using the value returned by this method in a loop, you can ask for the location of individual touches using the locationOfTouch:inView: method.

For example:

(Objective-C)

    for(int i=0; i<[panGestureRecogniser numberOfTouches]; i++)
{
CGPoint pt = [panGestureRecogniser locationOfTouch:i inView:self];
}

(Swift)

    for i in 0..<panGestureRecogniser.numberOfTouches {
let pt = recognizer.location(ofTouch: i, in: panGestureRecognizer.view)
}

As for velocity I believe there is only one value for it and there is no way to get the velocity of each touch without writing a custom method that calculates the difference between each touch over a series of calls. There is no guarantee that the touches will be at the same index each time however.

Note: As for the minimum and maximum number of touches, these will need to be set accordingly to get multiple touches.

Resolving conflicting touches with multitouch

Each "touch" in the UITouch Set that is passed to the function is a distinct touch event. When using multiple touches simultaneously, each finger would be represented with a separate "touch" in the set. Keep in mind that this is an unordered set.

In the code you provide, you retrieve the location of a single touch event. You check if the x value of the touch's location is high, then within that condition, you also check if it is low. But you're checking the same location, the same x value. If the x value is 768 or below, it will always be "!(x > 769)". If it is 769 or above, it will always be "not 768 or below".

Thus, for each touch event during the "move" function, no matter which side it's on, the other side is reset. If you have two touch events simultaneously on opposite sides, they are both constantly setting their side and resetting the other side. The resulting behavior will be "uncontrollable" as you said.

The Solution

You need to keep track of the state of your UI. Specifically, you need to track of your touch locations.

During touchesBegan(), you add the location to the set of tracked touches. During touchesEnded(), you remove the location from the set of tracked touches. During touchesMoved(), you determine which tracked touch is closest to the moved touch object, and update it accordingly.

Each time your set is updated, you'll parse your tracked touch set and update the rotations of your nodes accordingly. If a node has no touch that rotates it, it is then reset. This function would probably be best called after the for touch in touches loop.



Related Topics



Leave a reply



Submit