2

I am making a math related activity in which the user can draw with their fingers for scratch work as they try to solve the math question. However, I notice that when I move my finger quickly, the line lags behind my finger somewhat noticeably. I was wondering if there was some area I had overlooked for performance or if touchesMoved simply just doesn't come enough (it is perfectly smooth and wonderful if you don't move fast). I am using UIBezierPath. First I create it in my init method like this:

myPath=[[UIBezierPath alloc]init];
myPath.lineCapStyle=kCGLineCapSquare;
myPath.lineJoinStyle = kCGLineJoinBevel;
myPath.lineWidth=5;
myPath.flatness = 0.4;

Then in drawRect:

- (void)drawRect:(CGRect)rect
{
    [brushPattern setStroke];
    if(baseImageView.image)
    {
        CGContextRef c = UIGraphicsGetCurrentContext();
        [baseImageView.layer renderInContext:c];
    }

    CGBlendMode blendMode = self.erase ? kCGBlendModeClear : kCGBlendModeNormal;
    [myPath strokeWithBlendMode:blendMode alpha:1.0];
}

baseImageView is what I use to save the result so that I don't have to draw many paths (gets really slow after a while). Here is my touch logic:

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

    UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
    [myPath moveToPoint:[mytouch locationInView:self]];

}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{    
    UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
    [myPath addLineToPoint:[mytouch locationInView:self]];
    [self setNeedsDisplay];
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{    
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0f);
    CGContextRef c = UIGraphicsGetCurrentContext();
    [self.layer renderInContext:c];

    baseImageView.image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    [myPath removeAllPoints];
    [self setNeedsDisplay];
}

This project is going to be released as an enterprise app, so it will only be installed on iPad 2. Target iOS is 5.0. Any suggestions about how I can squeeze a little more speed out of this would be appreciated.

borrrden
  • 33,256
  • 8
  • 74
  • 109
  • Of course it's slow! UIBezierPath is slow as it is, and if you add a separate line every time you move your finger, then you will have many performance issues. My suggestion: Calculate the slope / length of the line, and if it is past a certain amount, then add the line. It will reduce smoothness a bit, but then you can add some interpolation if you'd like. – Richard J. Ross III May 29 '12 at 13:58
  • Adding to what Richard said, look closely at how most apps do this: While they are tracking motion, paths look a bit angled, then, once you lift your finger, paths are smoothed. – fzwo May 29 '12 at 14:05
  • Hope this will helpful. https://stackoverflow.com/questions/50718431/how-can-i-draw-smooth-freehand-drawing-using-shapelayer-and-bezierpath-over-an-i/58128300#58128300 – Lasantha Basnayake Sep 27 '19 at 05:29

2 Answers2

3

Of course you should start by running it under Instruments and look for your hotspots. Then you need to to make changes and re-evaluate to see their impact. Otherwise you're just guessing. That said, some notes from experience:

  • Adding lots of elements to a path can get very expensive. I would not be surprised if your addLineToPoint: turns out to be a hotspot. It has been for me.
  • Rather than backing your system with a UIImageView, I would probably render into a CGLayer. CGLayers are optimized for rendering into a specific context.
  • Why accumulate the path at all rather than just rendering it into the layer at each step? That way your path would never be more than two elements (move, addLine). Typically the two-stage approach is used so you can handle undo or the like.
  • Make sure that you're turning off any UIBezierPath features you don't want. In particular, look at the section "Accessing Draw Properties" in the docs. You may consider switching to CGMutablePath rather than UIBezierPath. It's not actually faster when configured the same, but it's default settings turn more things off, so by default it's faster. (You're already setting most of these; you'll want to experiment a little in Instruments to see what impact they make.)
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Very informative! I've never used CGLayer before so I will definitely look into that. – borrrden May 29 '12 at 23:43
  • Sorry, I'm getting such terrible performance out of this so I think I'm doing it wrong. You say "render to a CGLayer" but I don't really understand how to do that. The point of making a UIImage is so that I don't have to draw tons of paths into the context, but I don't see how to cache a CGLayer as pixel data, only how to draw into its context (which results in drawing long paths every time). So I end up rendering the layer into the context in every touch move (or else the user can't see what they are drawing). If you have any tips about this please clarify :). – borrrden May 30 '12 at 02:11
  • Treat the CGLayer as if it were a UIImage. It should be almost an exact replacement for the UIImage code you already have. You can keep appending to the CGLayer's context, just like you can keep drawing onto the UIImage. The only trick is that you need a graphics context to create the CGLayer in the first place. You do that by waiting until the first time drawRect: runs to create the CGLayer. After that, you can continue to use the same CGLayer forever, as long as its targeting the same kind of context (i.e. a view display context). – Rob Napier May 30 '12 at 12:50
  • Alright, I got it working I think, but wouldn't this way make the context more and more crowded when the user erases? When I rasterize it, it is just pixel data, but with paths every time the user erases and draws again it piles on top and bloats the context...is that right? – borrrden May 31 '12 at 01:47
  • You can think of a CGLayer as if it were an image. It just has a context attached to it. If you want to erase, however, you would just throw away the CGLayer and make a new one. That's easiest to do by using the context from the first to create the new one. Remember, when you CGLayerCreateWithContext, that doesn't mean that the layer uses *that* context. It means that the layer is optimized for that kind of context. – Rob Napier May 31 '12 at 14:04
  • I have to throw it away? I was just using the "clear" blend mode instead for a stroke eraser, and CGContextClearRect for full erase. I realized that the contexts are not the same, so in draw rect, I render the layer to the window context (which is now my biggest source of CPU cycles). However, the performance has gotten better. Thanks for the guidance! – borrrden May 31 '12 at 14:24
  • You're probably correct that clearing the rect is faster than creating a new context. It will be hard to get rid of the cost of that last CGContextDrawLayerInRect(). But do make sure you're compositing correctly (minimizing blending as much as possible). Look at what function is your particular problem; is it a blending function or a resampling color function for instance. – Rob Napier May 31 '12 at 14:38
  • 1
    Hello @Rob Napier, I am trying to perform erase operation and not clear all, I stroke using CGContextSetBlendMode(layerContext,kCGBlendModeClear); and erase only that part of the drawing that user wishes to erase and not whole drawing, and then I also clear the CGlayer when user starts to draw next time, but what happens is that, the previous left out drawing after erase also goes, I know why, because, I am clearing the CGlayer, but if I dont clear the CGLayer then when I draw next time, the erased part of the drawing would come back.So how we can takle this issue. – Ranjit Feb 09 '14 at 04:23
  • You should ask this as a new question, with some explanation of what you've already done. It is too complicated for a comment. – Rob Napier Feb 09 '14 at 17:09
1

http://mobile.tutsplus.com/tutorials/iphone/ios-sdk_freehand-drawing/

This link exactly shows how to make a curve smoother . This tutorial shows it step by step. And simply tells us how we can add some intermediate points (in touchesMoved method) to our curves to make them smoother.

YogiAR
  • 2,207
  • 23
  • 44