Question

As was suggested to me in a prior StackOverflow question, I'm trying to improve my drawing method for letting my user draw lines/dots into a UIView. I'm now trying to draw using a CAShapeLayer instead of dispatch_async. This all works correctly, however, drawing into the CAShapeLayer continuously while touches are moving becomes slow and the path lags behind, whereas my old (inefficient I was told) code worked beautifully smooth and fast. You can see my old code commented out below.

Is there any way to improve the performance for what I want to do? Maybe I'm overthinking something.

I'd appreciate any help offered.

Code:

@property (nonatomic, assign) NSInteger center;
@property (nonatomic, strong) CAShapeLayer *drawLayer;
@property (nonatomic, strong) UIBezierPath *drawPath;
@property (nonatomic, strong) UIView *drawView;
@property (nonatomic, strong) UIImageView *drawingImageView;
CGPoint points[4];

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];

    self.center = 0;
    points[0] = [touch locationInView:self.drawView];

    if (!self.drawLayer)
    {
        CAShapeLayer *layer = [CAShapeLayer layer];
        layer.lineWidth = 3.0;
        layer.lineCap = kCALineCapRound;
        layer.strokeColor = self.inkColor.CGColor;
        layer.fillColor = [[UIColor clearColor] CGColor];
        [self.drawView.layer addSublayer:layer];
        self.drawView.layer.masksToBounds = YES;
        self.drawLayer = layer;
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
   UITouch *touch = [touches anyObject];

   self.center++;
   points[self.center] = [touch locationInView:self.drawView];

   if (self.center == 3)
   {
       UIBezierPath *path = [UIBezierPath bezierPath];

       points[2] = CGPointMake((points[1].x + points[3].x)/2.0, (points[1].y + points[3].y)/2.0);
       [path moveToPoint:points[0]];
       [path addQuadCurveToPoint:points[2] controlPoint:points[1]];
       points[0] = points[2];
       points[1] = points[3];
       self.center = 1;

       [self drawWithPath:path];
    }
}

- (void)drawWithPath:(UIBezierPath *)path
{
    if (!self.drawPath)
    {
        self.drawPath = [UIBezierPath bezierPath];
    }

    [self.drawPath appendPath:path];

    self.drawLayer.path = self.drawPath.CGPath;

    [self.drawLayer setNeedsDisplay];

    // Below code worked faster and didn't lag behind at all really

    /*

    dispatch_async(dispatch_get_main_queue(),
    ^{
        UIGraphicsBeginImageContextWithOptions(self.drawingImageView.bounds.size, NO, 0.0);

        [self.drawingImageView.image drawAtPoint:CGPointZero];

        [self.inkColor setStroke];
        [path stroke];

        self.drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    });

    */
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (self.center == 0)
    {
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:points[0]];
        [path addLineToPoint:points[0]];

        [self drawWithPath:path];
    }

    self.drawLayer = nil;
    self.drawPath = nil;
}
Was it helpful?

Solution

This problem intrigued me as I've always found UIBezierPath/shapeLayer to be reletivly fast.

It's important to note that in your code above, you continues to add points to drawPath. As this increases, the appendPath method becomes a real resource burden. Similarly, there is no point in successively rendering the same points over and over.

As a side note, there is a visible performance difference when increasing lineWidth and adding lineCap (regardless of approach). For the sake of comparing Apples with Apples, in the test below, I've left both to default values.

I took your above code and changed it a little. The technique I've used is to add touchPoints to the BezierPath up to a per-determined number, before committing the current rendering to image. This is similar to your original approach, however, given that it's not happening with every touchEvent. it's far less CPU intensive. I tested both approaches on the slowest device I have (iPhone 4S) and noted that CPU utilization on your initial implementation was consistently around 75-80% whilst drawing. Whilst with the modified/CAShapeLayer approach, CPU utilization was consistently around 10-15% Memory usage also remained minimal with the second approach.

Below is the Code I used;

@interface MDtestView () // a simple UIView Subclass
@property (nonatomic, assign) NSInteger cPos;
@property (nonatomic, strong) CAShapeLayer *drawLayer;
@property (nonatomic, strong) UIBezierPath *drawPath;
@property (nonatomic, strong) NSMutableArray *bezierPoints;
@property (nonatomic, assign) NSInteger pointCount;
@property (nonatomic, strong) UIImageView *drawingImageView;
@end


@implementation MDtestView
CGPoint points[4];

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
    // Initialization code
    }
    return self;
}

-(id)initWithCoder:(NSCoder *)aDecoder{
    self = [super initWithCoder:aDecoder];
    if (self) {
    //
    }
    return self;
 }



- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];

    self.cPos = 0;
    points[0] = [touch locationInView:self];

    if (!self.drawLayer)
    {
        // this should be elsewhere but kept it here to follow your code
        self.drawLayer = [CAShapeLayer layer];
        self.drawLayer.backgroundColor = [UIColor clearColor].CGColor;
        self.drawLayer.anchorPoint = CGPointZero;
        self.drawLayer.frame = self.frame;
        //self.drawLayer.lineWidth = 3.0;
       // self.drawLayer.lineCap = kCALineCapRound;
        self.drawLayer.strokeColor = [UIColor redColor].CGColor;
        self.drawLayer.fillColor = [[UIColor clearColor] CGColor];
        [self.layer  insertSublayer:self.drawLayer above:self.layer ];

        self.drawingImageView = [UIImageView new];
        self.drawingImageView.frame = self.frame;
        [self addSubview:self.drawingImageView];
    }
}

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

    UITouch *touch = [touches anyObject];
    if (!self.drawPath)
    {
        self.drawPath = [UIBezierPath bezierPath];
      //  self.drawPath.lineWidth = 3.0;
      //  self.drawPath.lineCapStyle = kCGLineCapRound;
    }

    // grab the current time for testing Path creation and appending
    CFAbsoluteTime cTime = CFAbsoluteTimeGetCurrent();

    self.cPos++;
    //points[self.cPos] = [touch locationInView:self.drawView];
    points[self.cPos] = [touch locationInView:self];
    if (self.cPos == 3)
    {


    /* uncomment this block to test old method


       UIBezierPath *path = [UIBezierPath bezierPath];

       [path moveToPoint:points[0]];
       points[2] = CGPointMake((points[1].x + points[3].x)/2.0, (points[1].y + points[3].y)/2.0);
       [path addQuadCurveToPoint:points[2] controlPoint:points[1]];
        points[0] = points[2];
        points[1] = points[3];
        self.cPos = 1;
        dispatch_async(dispatch_get_main_queue(),
                       ^{
                           UIGraphicsBeginImageContextWithOptions(self.drawingImageView.bounds.size, NO, 0.0);

                           [self.drawingImageView.image drawAtPoint:CGPointZero];
                          // path.lineWidth = 3.0;
                         //  path.lineCapStyle = kCGLineCapRound;
                           [[UIColor redColor] setStroke];
                           [path stroke];

                           self.drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext();
                           UIGraphicsEndImageContext();
                           NSLog(@"it took %.2fms to draw via dispatchAsync", 1000.0*(CFAbsoluteTimeGetCurrent() - cTime));
                   });
   */

    // I've kept the original structure in place, whilst comparing apples for apples. we really don't need to create
    // a new bezier path and append it. We can simply add the points to the global drawPath, and zero it at an
    // appropriate point. This would also eliviate the need for appendPath
    // /*
        [self.drawPath moveToPoint:points[0]];
        points[2] = CGPointMake((points[1].x + points[3].x)/2.0, (points[1].y + points[3].y)/2.0);
        [self.drawPath addQuadCurveToPoint:points[2] controlPoint:points[1]];

        points[0] = points[2];
        points[1] = points[3];
        self.cPos = 1;
        self.drawLayer.path = self.drawPath.CGPath;

         NSLog(@"it took %.2fms to render %i bezier points", 1000.0*(CFAbsoluteTimeGetCurrent() - cTime), self.pointCount);

       // 1 point for MoveToPoint, and 2 points for addQuadCurve
        self.pointCount += 3;

         if (self.pointCount > 100) {
            self.pointCount = 0;
            [self commitCurrentRendering];
        }

  //  */
    }
}

- (void)commitCurrentRendering{
    CFAbsoluteTime cTime = CFAbsoluteTimeGetCurrent();
    @synchronized(self){
        CGRect paintLayerBounds = self.drawLayer.frame;
        UIGraphicsBeginImageContextWithOptions(paintLayerBounds.size, NO, [[UIScreen mainScreen]scale]);
        CGContextRef context = UIGraphicsGetCurrentContext();
        CGContextSetBlendMode(context, kCGBlendModeCopy);
        [self.layer renderInContext:context];
        CGContextSetBlendMode(context, kCGBlendModeNormal);
        [self.drawLayer renderInContext:context];
        UIImage *previousPaint = UIGraphicsGetImageFromCurrentImageContext();

        self.layer.contents = (__bridge id)(previousPaint.CGImage);
        UIGraphicsEndImageContext();
        [self.drawPath removeAllPoints];
    }
    NSLog(@"it took %.2fms to save the context", 1000.0*(CFAbsoluteTimeGetCurrent() - cTime));
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (self.cPos == 0)
    {
      /* //not needed
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:points[0]];
        [path addLineToPoint:points[0]];
        [self drawWithPath:path];
     */
    }
    if (self.cPos == 2) {
        [self commitCurrentRendering];
      }

   // self.drawLayer = nil;
    [self.drawPath removeAllPoints];
}

@end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top