1

Need a fresh pair of eyes, mine have stopped working and can't make sense of my own code anymore...

I am trying to make a drawing app with a pen on paper style of drawing. Here's how it is suppose to work:

  • user touches, app grabs location
  • a var is set to tell drawRect to configure CGcontext, create a new path, move to point, etc (because I always gett errors/warnings in my nslog whenever I do anything with CGcontext outside the drawRect method)
  • var is then set to determine whether to place a dot or a line when the user lifts finger
  • if the user drags, the var is changed to tell drawRect to draw a line and [self setneedsdisplay] is called
  • every time the user places their finger on the screen a timer is activated, every 5 seconds (or until they lift their finger up) the app 'captures' the screen contents, sticks it in an image and wipes the screen, replacing the image and continues drawing (in effect cache'ing the image so all these lines don't have to be re-drawn)

UPDATE #1 So I re-wrote it (because it was obviously really bad and not working...) and ended up with this:

- (void)drawRect:(CGRect)rect {
    [_incrImage drawInRect:rect]; /* draw image... this will be blank at first 
    and then supposed to be filled by the Graphics context so that when the view 
    is refreshed the user is adding lines ontop of an image (but to them it looks 
    like a continuous drawing)*/
    CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapSquare);
    CGContextSetLineJoin(UIGraphicsGetCurrentContext(), kCGLineJoinRound);
    CGContextSetLineWidth(UIGraphicsGetCurrentContext(), _brushW);
    CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), [UIColor colorWithRed:_brushR green:_brushG blue:_brushB alpha:_brushO].CGColor);
    CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeNormal);
    CGContextAddPath(UIGraphicsGetCurrentContext(), _pathref);
    CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathStroke);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    _drw = 2; //set this to draw a dot if the user taps
    UITouch *touch = [touches anyObject];
    _cp = [touch locationInView:self];
    _lp = _cp;
    CGPathRelease(_pathref); /* this line and the line below is to clear the 
    path so the app is drawing as little as possible (the idea is that as 
    the user draws and lifts their finger, the drawing goes into _incrImage 
    and then the contents is cleared and _incrImage is applied to the 
    screen so there is as little memory being used as possible and so the 
    user feels as if they're adding to what they've drawn when they touch 
    the device again) */
    _pathref = CGPathCreateMutable();
    CGPathMoveToPoint(_pathref, NULL, _lp.x, _lp.y);
    touch = nil;
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    _drw = 1; //user moved their finger, this was not a tap so they want 
              //to draw a line
    UITouch *touch = [touches anyObject];
    _cp = [touch locationInView:self];
    CGPathAddLineToPoint(_pathref, NULL, _cp.x, _cp.y);
    [self setNeedsDisplay]; //as the user moves their finger, it draws
    _lp = _cp;
    touch = nil;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    _cp = [touch locationInView:self];
    switch (_drw) { //logic to determine what to draw when the user 
                    //lifts their finger
         case 1:
            //line
            CGPathAddLineToPoint(_pathref, NULL, _cp.x, _cp.y);

            break;
         case 2:
            //dot
            CGPathAddArc(_pathref, NULL, _cp.x, _cp.y, _brushW, 0.0, 360.0, 1);

            break;

            default:
            break;
    }
    /* here's the fun bit, the Graphics context doesn't seem to be going 
    into _incrImage and therefore is not being displayed when the user 
    goes to continue drawing after they've drawn a line/dot on the screen */
    UIGraphicsBeginImageContext(self.frame.size);
    CGContextAddPath(UIGraphicsGetCurrentContext(), _pathref); /* tried adding
    my drawn path to the context and then adding the context to the image 
    before the user taps down again and the path is cleared... not sure 
    why it isn't working. */
    [_incrImage drawAtPoint:CGPointZero];
    _incrImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    [self setNeedsDisplay]; //finally, refresh the contents
    touch = nil;
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [self touchesEnded:touches withEvent:event];
}

My new problem is that everything is erased when drawRect is called and that '_incrImage' is not getting the contents of the current graphics and displaying them

__old, I guess no longer needed but keeping here for reference___

here is the relevant code:

- (void)drawRect:(CGRect)rect {
/* REFERENCE: _drw: 0 = clear everything
                    1 = cache the screen contents in the image and display that
                        so the device doesn't have to re-draw everything
                    2 = set up new path
                    3 = draw lines instead of a dot at current point
                    4 = draw a 'dot' instead of a line
*/
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapSquare);
CGContextSetLineJoin(UIGraphicsGetCurrentContext(), kCGLineJoinRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), _brushW);
CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(), [UIColor colorWithRed:_brushR green:_brushG blue:_brushB alpha:_brushO].CGColor);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeNormal);

switch (_drw) {
    case 0:
        //clear everything...this is the first draw... it can also be called to clear the view
        UIGraphicsBeginImageContext(self.frame.size);
        CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeClear);
        CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [UIColor clearColor].CGColor);
        CGContextFillRect(UIGraphicsGetCurrentContext(), self.frame);
        CGContextFlush(UIGraphicsGetCurrentContext());
        _incrImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        [_incrImage drawAtPoint:CGPointZero];
        [_incrImage drawInRect:rect];
        break;
    case 1:
        //capture the screen content and stick it in _incrImage... 
        then apply_incrImage to screen so the user can continue drawing ontop of it
        _incrImage = UIGraphicsGetImageFromCurrentImageContext();
        [_incrImage drawAtPoint:CGPointZero];
        [_incrImage drawInRect:rect];
        break;
    case 2:
        //begin path and set everything up, this is called when touchesBegan: fires...
        _incrImage = UIGraphicsGetImageFromCurrentImageContext();
        [_incrImage drawAtPoint:CGPointZero];
        [_incrImage drawInRect:rect];
        CGContextBeginPath(UIGraphicsGetCurrentContext());
        CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _p.x, _p.y);
        break;
    case 3:
        //add lines, this is after the path is created and set...this is fired when touchesMoved: gets activated and _drw is set to draw lines instead of adding dots
        CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), _p.x, _p.y);
        CGContextStrokePath(UIGraphicsGetCurrentContext());
        break;
    case 4:
        //this is fired when touchesEnd: is activated... this sets up ready if the app needs to draw a 'dot' or the arc with a fill...
        CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _p.x, _p.y);
        CGContextAddArc(UIGraphicsGetCurrentContext(), _p.x, _p.y, _brushW, 0.0, 360.0, 1);
        CGContextFillPath(UIGraphicsGetCurrentContext());
        CGContextFlush(UIGraphicsGetCurrentContext());
        break;

    default:
        break;
  }
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    dTimer = [NSTimer timerWithTimeInterval:5.0 target:self selector:@selector(timeUP) userInfo:Nil repeats:YES];
    _drw = 2;
    UITouch *touch = [touches anyObject];
    _p = [touch locationInView:self];
    [self setNeedsDisplay];
    _drw = 4;
}
- (void)timeUP {
    _drw = 1;
    [self setNeedsDisplay];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    _drw = 3;
    UITouch *touch = [touches anyObject];
    _p = [touch locationInView:self];
    [self setNeedsDisplay];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event // (2)
{
    [dTimer invalidate];
    UITouch *touch = [touches anyObject];
    _p = [touch locationInView:self];
    [self setNeedsDisplay];
    [self timeUP];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [self touchesEnded:touches withEvent:event];
}

My questions are:

  1. is this efficient? or is there a better way to do this? and
  2. Why is this not drawing? I get nothing but I can see my memory being used...
blindman457
  • 293
  • 1
  • 11

2 Answers2

3

Egad.

Your first problem is that setNeedsDisplay doesn’t draw immediately, it just marks the view to be drawn at the end of the event. So, when you set _drw=2 and call setNeedsDisplay and then set _drw=4, it’s only going to actually call -drawRect: with _drw=4 (if that, since that still might not be the end of the current event).

But, also don’t, uh, use that _drw switch thing. That’s not good.

You want to create an image and draw into the image as the touches happen, and then in drawRect: just blat the image to the screen. If you ever find yourself calling UIGraphicsGetImageFromCurrentImageContext() inside -drawRect; you are doing things backwards (as you are here). Don’t slurp the image from the screen, create an image that you blat to the screen.

The screen should never been your ‘state’. That way lies madness.

Wil Shipley
  • 9,343
  • 35
  • 59
  • Ah okay! So as you can obviously figure out I may be a little new to this, thanks for the tips! (setneeds display makes sense) I thought I had to use some sort of logic because every time I try and set any type of CGcontext outside the drawRect method I can one a CGcontext 'whatever I'm trying to do' invalid context error... Thanks for the clarity though. So my new query is: How can I create and only update certain parts of the image (ie: add new drawn lines) but also alter all of it (wipe it clean for example, or change every line's colour)? Anyway, thanks for the help! – blindman457 Feb 10 '14 at 10:35
1

On top of the answer from @WilShipley which I agree with (don't put state management logic in drawRect:, only pure redrawing logic), you are currently never drawing drawing anything other than a minuscule line because CGContextStrokePath clears the current line from the context.

The context isn't intended to be a temporary cache of incomplete drawing operations, it's your portal to the screen (or a backing image / PDF file / ...). You need to create your drawing state outside drawRect: and then render it to the screen inside drawRect:.

To ease performance issues, only redraw the area around the newest touch (or between the newest and previous touch).

Wain
  • 118,658
  • 15
  • 128
  • 151
  • yeah, I had a hunch state management was bad... but like I said, I get errors when trying to add things to the context outside the drawRect method... And the idea was that it didn't matter if anything was being cleared because all drawn points were to be cached in an image that was drawn behind the user (so to them it looked like they were just adding to a canvas) Just a question though, how would I find out what sections only needed to be re-drawn and just re-draw those parts? – blindman457 Feb 10 '14 at 10:38
  • You need to create your own context if outside of `drawRect:` (`CGBitmapContextCreate`). Redrawing is based on the current point and the previous the user is / was touching (because that is where the new line segment will be created). – Wain Feb 10 '14 at 10:43
  • Okay, it may be that I'm tired at the moment and not thinking straight, but how would I go about doing that? and I'm not understanding properly about how re-drawing works sorry. – blindman457 Feb 10 '14 at 11:03
  • 1
    http://stackoverflow.com/questions/8100845/turn-two-cgpoints-into-a-cgrect (create the area to be redrawn). I would keep an instance variable that is the 'current path', created when the user touches down, modified and destroyed when the user removes the touch. This is used when the touch moves to create / update your image (which may prove to be a little inefficient, look at only creating a new image when a touch ends). Draw rect just draws the image (and possibly the current path). – Wain Feb 10 '14 at 11:16