Search code examples
iosdrawrectuitouch

How to continue to drawRect: when finger on screen


I have the current code:

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

    self.objectPoint = [[touches anyObject] locationInView:self];
    float x, y;
    if (self.objectPoint.x > self.objectPoint.x) {
        x = self.objectPoint.x + 1;
    }
    else x = self.objectPoint.x - 1;
    if (self.fingerPoint.y > self.objectPoint.y) {
        y = self.objectPoint.y + 1;
    }
    else y = self.minionPoint.y - 1;
    self.objectPoint = CGPointMake(x, y);

    [self setNeedsDisplay];
}

My problem is that I want to keep the object follow your finger until you take your finger off the screen. It will only follow if my finger is moving. touchesEnded only works when I take my finger off the screen, so that's not what I want either. How can I enable something that would solve my problem?


Solution

  • If you want to touch a part of the screen and you want to move the drawn object in that direction as long as you're holding your finger down, there are a couple of approaches.

    On approach is the use of some form of timer, something that will repeatedly call a method while the user is holding their finger down on the screen (because, as you noted, you only get updates to touchesMoved when you move). While NSTimer is the most common timer that you'd encounter, in this case you'd want to use a specialized timer called a display link, a CADisplayLink, that fires off when screen updates can be performed. So, you would:

    • In touchesBegan, capture where the user touched on the screen and start the CADisplayLink;

    • In touchesMoved, you'd update the user's touch location (but only called if they moved their finger);

    • In touchesEnded, you'd presumably stop the display link; and

    • In your CADisplayLink handler, you'd update the location (and you'd need to know the speed with which you want it to move).

    So, that would look like:

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
        self.velocity = 100.0;     // 100 points per second
        self.touchLocation = [[touches anyObject] locationInView:self];
    
        [self startDisplayLink];
    }
    
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
    {
        self.touchLocation = [[touches anyObject] locationInView:self];
    }
    
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
    {
        [self stopDisplayLink];
    }
    
    - (void)startDisplayLink
    {
        self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
        self.lastTimestamp = CACurrentMediaTime();   // initialize the `lastTimestamp`
        [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    }
    
    - (void)stopDisplayLink
    {
        [self.displayLink invalidate];
        self.displayLink = nil;
    }
    
    - (void)handleDisplayLink:(CADisplayLink *)displayLink
    {
        // figure out the time elapsed, and reset the `lastTimestamp`
    
        CFTimeInterval currentTimestamp = CACurrentMediaTime();
        CFTimeInterval elapsed = currentTimestamp - self.lastTimestamp;
        self.lastTimestamp = currentTimestamp;
    
        // figure out distance to touch and distance we'd move on basis of velocity and elapsed time
    
        CGFloat distanceToTouch  = hypotf(self.touchLocation.y - self.objectPoint.y, self.touchLocation.x - self.objectPoint.x);
        CGFloat distanceWillMove = self.velocity * elapsed;
    
        // this does the calculation of the angle between the touch location and
        // the current `self.objectPoint`, and then updates `self.objectPoint` on
        // the basis of (a) the angle; and (b) the desired velocity.
    
        if (distanceToTouch == 0.0)                  // if we're already at touchLocation, then just quit
            return;
        if (distanceToTouch < distanceWillMove) {    // if the distance to move is less than the target, just move to touchLocation
            self.objectPoint = self.touchLocation;
        } else {                                     // otherwise, calculate where we're going to move to
            CGFloat angle = atan2f(self.touchLocation.y - self.objectPoint.y, self.touchLocation.x - self.objectPoint.x);
            self.objectPoint = CGPointMake(self.objectPoint.x + cosf(angle) * distanceWillMove,
                                           self.objectPoint.y + sinf(angle) * distanceWillMove);
        }
        [self setNeedsDisplay];
    }
    

    and to use that, you'd need a few properties defined:

    @property (nonatomic) CGFloat velocity;
    @property (nonatomic) CGPoint touchLocation;
    @property (nonatomic, strong) CADisplayLink *displayLink;
    @property (nonatomic) CFTimeInterval lastTimestamp;