objective-ciosuibezierpathtouchesbegantouchesmoved

Stretchy UIBezierPath line?


I want to draw a straight line with my finger that automatically sizes based upon how far away I am from the point of origin.

So if I touch the screen in the middle and slide my finger out a line appears to 'stretch' and pivot around the point of orgin as my finger moves on the screen. WHhen I lift my finger. The Destination Point should finalize and create a line. I can drag my finger across the screen and 'Draw' on the screen but that's not what I am wanting to do.

I thought UIBeizerPath moveToPoint would help but it just messes things up.

What am I doing wrong?

- (id)initWithFrame:(CGRect)frame
{
     //default line properties
    myPath=[[UIBezierPath alloc]init];
    myPath.lineCapStyle=kCGLineCapRound;
    myPath.miterLimit=0;
    myPath.lineWidth=lineWidth;
    brushPattern=[UIColor blackColor];
 }



-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    CGPoint curPoint = [[touches anyObject] locationInView:self];

    lastPoint = curPoint;

    [myPath moveToPoint:lastPoint];
    [pathArray addObject:myPath];
    [self setNeedsDisplay];
}

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

    CGPoint curPoint = [[touches anyObject] locationInView:self];

    myPath.lineWidth=lineWidth;
    brushPattern=[UIColor redColor]; //red to show it hasn't been added yet.
    [myPath moveToPoint:tempPoint];
    [self setNeedsDisplay];
}

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

    CGPoint curPoint = [[touches anyObject] locationInView:self];

    myPath.lineWidth=lineWidth;

    brushPattern=[UIColor blackColor]; //finalize the line with black color
    [myPath addLineToPoint:curPoint];
    [self setNeedsDisplay];
}

Solution

  • Here's one concept. Draws a line from where you start dragging your finger until where you let go, animating it as you drag your finger around. It does this by making a CAShapeLayer, resetting the path as you move your finger around.

    This should demonstrate the basic idea:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
        [self.view addGestureRecognizer:gesture];
    }
    
    - (CAShapeLayer *)createShapeLayer:(UIView *)view {
        CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] init];
    
        shapeLayer.fillColor = [UIColor clearColor].CGColor;
        shapeLayer.strokeColor = [UIColor redColor].CGColor;
        shapeLayer.lineWidth = 3.0;
    
        [view.layer addSublayer:shapeLayer];
    
        return shapeLayer;
    }
    
    - (void)handlePanGesture:(UIPanGestureRecognizer *)gesture {
        static CAShapeLayer *shapeLayer;
        static CGPoint origin;
    
        if (gesture.state == UIGestureRecognizerStateBegan) {
            shapeLayer = [self createShapeLayer:gesture.view];
            origin = [gesture locationInView:gesture.view];
        } else if (gesture.state == UIGestureRecognizerStateChanged) {
            UIBezierPath *path = [UIBezierPath bezierPath];
            [path moveToPoint:origin];
            CGPoint location = [gesture locationInView:gesture.view];
            [path addLineToPoint:location];
            shapeLayer.path = path.CGPath;
        } else if (gesture.state == UIGestureRecognizerStateEnded ||
                   gesture.state == UIGestureRecognizerStateFailed ||
                   gesture.state == UIGestureRecognizerStateCancelled) {
            shapeLayer = nil;
        }
    }
    

    Or, in Swift 3:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        view.addGestureRecognizer(pan)
    }
    
    private func createShapeLayer(for view: UIView) -> CAShapeLayer {
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor.red.cgColor
        shapeLayer.lineWidth = 3.0
    
        view.layer.addSublayer(shapeLayer)
    
        return shapeLayer
    }
    
    private var shapeLayer: CAShapeLayer!
    private var origin: CGPoint!
    
    func handlePan(_ gesture: UIPanGestureRecognizer) {
        if gesture.state == .began {
            shapeLayer = createShapeLayer(for: gesture.view!)
            origin = gesture.location(in: gesture.view)
        } else if gesture.state == .changed {
            let path = UIBezierPath()
            path.move(to: origin)
            path.addLine(to: gesture.location(in: gesture.view))
            shapeLayer.path = path.cgPath
        } else if gesture.state == .ended || gesture.state == .failed || gesture.state == .cancelled {
            shapeLayer = nil
        }
    }
    

    If you don't use CAShapeLayer, but you want to keep track of previous paths, you'll have to maintain an array for those old paths, and build a path that consists of all of the old paths, perhaps something like:

    @interface CustomView ()
    
    @property (nonatomic) CGPoint originPoint;
    @property (nonatomic) CGPoint currentPoint;
    @property (nonatomic) NSMutableArray *previousPaths;
    
    @end
    
    @implementation CustomView
    
    - (id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self configure];
        }
        return self;
    }
    
    - (id)init {
        return [self initWithFrame:CGRectZero];
    }
    
    - (id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self configure];
        }
        return self;
    }
    
    - (void)configure {
        _previousPaths = [[NSMutableArray alloc] init];
    }
    
    - (void)drawRect:(CGRect)rect {
        [[UIColor redColor] setStroke];
    
        UIBezierPath *drawPath = [UIBezierPath bezierPath];
    
        drawPath.lineCapStyle = kCGLineCapRound;
        drawPath.miterLimit = 0;
        drawPath.lineWidth = 3.0;
    
        for (UIBezierPath *path in self.previousPaths)
            [drawPath appendPath:path];
    
        UIBezierPath *path = [self pathForCurrentLine];
        if (path)
            [drawPath appendPath:path];
    
        [drawPath stroke];
    }
    
    - (UIBezierPath *)pathForCurrentLine {
        if (CGPointEqualToPoint(self.originPoint, CGPointZero) && CGPointEqualToPoint(self.currentPoint, CGPointZero))
            return nil;
    
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:self.originPoint];
        [path addLineToPoint:self.currentPoint];
    
        return path;
    }
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
        self.originPoint = [[touches anyObject] locationInView:self];
    }
    
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
        UITouch *touch = [touches anyObject];
        if ([event respondsToSelector:@selector(predictedTouchesForTouch:)]) {
            touch = [[event predictedTouchesForTouch:touch] lastObject] ?: touch;
        }
        self.currentPoint = [touch locationInView:self];
    
        [self setNeedsDisplay];
    }
    
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
        self.currentPoint = [[touches anyObject] locationInView:self];
        [self.previousPaths addObject:[self pathForCurrentLine]];
        self.originPoint = self.currentPoint = CGPointZero;
    
        [self setNeedsDisplay];
    }
    
    @end