Doing Undo and Redo with Cglayer Drawing

1.5k views Asked by At

I am working with a drawing app, I am using CGlayers for drawing. On touches ended, I get image out of the layer and store it in a Array, which I use to undo operation.

My touches ended function

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{    
    NSLog(@"Touches ended");

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0.0);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawLayerInRect(context, self.bounds, self.drawingLayer);
    m_curImage = UIGraphicsGetImageFromCurrentImageContext();  
    UIGraphicsEndImageContext(); 

    [m_undoArray addObject: m_curImage];
}

My Drawing View expands dynamically on user demand, So suppose user can draw say, a line with drawView size 200*200, then expand it to 200*300 and draw one more line, then expand it to 200*300 and draw one more line.

Here is the image of the app

So now I have 3 images with different sizes in UndoArray.

Whenever I increase/decrese the canvas size. I have written this code

On increase and decrease of the drawingView, I am writing this function

 (void)increaseDecreaseDrawingView 
{ 
self.currentDrawingLayer = nil; 

if(self.permanentDrawingLayer) 
{ 
rectSize = self.bounds; 
NSLog(@"Size%@", NSStringFromCGRect(self.bounds)); 
CGContextRef context = UIGraphicsGetCurrentContext(); 

//self.newDrawingLayer = CGLayerCreateWithContext(context, self.bounds.size, NULL); 
CGFloat scale = self.contentScaleFactor; 
CGRect bounds = CGRectMake(0, 0, self.bounds.size.width * scale, self.bounds.size.height * scale); 
CGLayerRef layer = CGLayerCreateWithContext(context, bounds.size, NULL); 
CGContextRef layerContext = CGLayerGetContext(layer); 
CGContextScaleCTM(layerContext, scale, scale); 
self.newDrawingLayer = layer; 


CGContextDrawLayerInRect(layerContext, self.bounds, self.permanentDrawingLayer ); 

self.permanentDrawingLayer = nil; 

} 

And for doing undo I have written this code

- (void)Undo
{
     //Destroy the layer and create it once again with the image you get from undoArray.
     self.currentDrawingLayer = Nil;

     CGContextRef layerContext1 = CGLayerGetContext(self.permanentDrawingLayer );
     CGContextClearRect(layerContext1, self.bounds);

     CGContextRef context = UIGraphicsGetCurrentContext();

    for(int i =0; i<[m_rectArrayUndo count];i++)
    {
        CGRect rect = [[m_rectArrayUndo objectAtIndex:i]CGRectValue];
        CGLayerRef undoLayer = CGLayerCreateWithContext(context, rect.size, NULL);

        CGContextRef layerContext = CGLayerGetContext(undoLayer );
        CGContextTranslateCTM(layerContext, 0.0, rect.size.height);
        CGContextScaleCTM(layerContext, 1.0, -1.0);

        CGRect imageFrame;

        NSDictionary *lineInfo = [m_undoArray objectAtIndex:i];
        m_curImage = [lineInfo valueForKey:@"IMAGE"];
       imageFrame = CGRectMake(0 ,0,m_curImage.size.width,m_curImage.size.height);
       CGContextDrawImage(layerContext, imageFrame, m_curImage.CGImage);
       CGContextDrawLayerInRect(context, rect, undoLayer );
       CGContextDrawLayerInRect(layerContext1, rect, undoLayer);
    }          
}

In my drawRect function, I have written this code

- (void)drawRect:(CGRect)rect
{    

            CGContextRef context = UIGraphicsGetCurrentContext();//Get a reference to current context(The context to draw)

            if(self.currentDrawingLayer == nil)
            {                
                CGLayerRef layer = CGLayerCreateWithContext(context, bounds.size, NULL);                             
                self.currentDrawingLayer = layer;
            }



            if(self.permanentDrawingLayer == nil)
            {
                CGLayerRef layer = CGLayerCreateWithContext(context, bounds.size, NULL);
                self.permanentDrawingLayer = layer;
            }


            if(self.newDrawingLayer == nil)
            {
                CGLayerRef layer = CGLayerCreateWithContext(context, bounds.size, NULL);
                self.newDrawingLayer = layer;
            }

            CGPoint mid1 = midPoint(m_previousPoint1, m_previousPoint2);
            CGPoint mid2 = midPoint(m_currentPoint, m_previousPoint1);



            CGContextRef layerContext = CGLayerGetContext(self.currentDrawingLayer);

            CGContextSetLineCap(layerContext, kCGLineCapRound);
            CGContextSetBlendMode(layerContext, kCGBlendModeNormal);
            CGContextSetLineJoin(layerContext, kCGLineJoinRound);
            CGContextSetLineWidth(layerContext, self.lineWidth);
            CGContextSetStrokeColorWithColor(layerContext, self.lineColor.CGColor);
            CGContextSetShouldAntialias(layerContext, YES);
            CGContextSetAllowsAntialiasing(layerContext, YES);
            CGContextSetAlpha(layerContext, self.lineAlpha);
            CGContextSetFlatness(layerContext, 1.0f);
            CGContextBeginPath(layerContext);
            CGContextMoveToPoint(layerContext, mid1.x, mid1.y);//Position the current point
            CGContextAddQuadCurveToPoint(layerContext, m_previousPoint1.x, m_previousPoint1.y, mid2.x, mid2.y);
            CGContextStrokePath(layerContext);//paints(fills) the line along the current path.

            CGContextDrawLayerInRect(context, self.bounds, self.newDrawingLayer);

            CGContextDrawLayerInRect(context,  self.bounds, self.permanentDrawingLayer);
            CGContextDrawLayerInRect(context, self.bounds, self.currentDrawingLayer);

            [super drawRect:rect];
}

I have few doubts

  1. Is this the correct way to do? Or is their any better approach.

  2. Here What happens is that, my images from undo array are not respecting the rects and are drawn at any random position on the new Layer.

So I want to know how we can draw them properly so that images are drawn properly on CGlayers at specific position.

2

There are 2 answers

5
Jef On

An image object for each touch event is a bad idea IMHO, you're tearing through ram. Why not keep an array of touch points and draw dynamically? Easy enough to remove the last few elements from that array for a cheap undo operation

////14 Jan 2014// //edit to include example//

OK here is a quick drawing view example. there are three mutableArrays, _touches, which is for all previous drawings, _currentTouch, which is the current drawing and only contains data during touch events, (between touches began and touches ended).. and a redo array that data which is removed by undo is copied to rather than just deleting it (which you can certainly do)

enjoy :)

//
//  JEFdrawingViewExample.m
//  Created by Jef Long on 14/01/2014.
//  Copyright (c) 2014 Jef Long / Dragon Ranch. All rights reserved.
//

#import "JEFdrawingViewExample.h"
///don't worry, the header is empty :)
/// this is a subclass of UIView

@interface JEFdrawingViewExample()

-(UIColor *)colourForLineAtIndex:(int)lineIndex;
//swaps the coulour for each line

-(void)undo;
-(void)redo;

@end;


@implementation JEFdrawingViewExample
{
//iVars
  NSMutableArray *_touches;
  NSMutableArray *_currentTouch;
  NSMutableArray *_redoStore;
  }

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
      _touches = [[NSMutableArray alloc]init];
      _currentTouch = [[NSMutableArray alloc]init];
      _redoStore = [[NSMutableArray alloc]init];
    }
    return self;
}

#pragma mark - touches
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
  UITouch *touch = [touches anyObject];
  CGPoint touchPoint = [touch locationInView:self];

  [_currentTouch removeAllObjects];

  [_currentTouch addObject:NSStringFromCGPoint(touchPoint)];
  ///there are other, possibly less expensive ways to do this.. (adding a CGPoint to an NSArray.)
  // typecasting to (id) doesnt work under ARC..
  // two NSNumbers probably not any cheaper..

}

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

  UITouch *touch = [touches anyObject];
  CGPoint touchPoint = [touch locationInView:self];
  [_currentTouch addObject:NSStringFromCGPoint(touchPoint)];
  [self setNeedsDisplay];
  }

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

  UITouch *touch = [touches anyObject];
  CGPoint touchPoint = [touch locationInView:self];
  [_currentTouch addObject:NSStringFromCGPoint(touchPoint)];
  [_touches addObject:[NSArray arrayWithArray:_currentTouch]];
  [_currentTouch removeAllObjects];
  [self setNeedsDisplay];
}

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

  [_currentTouch removeAllObjects];
  [self setNeedsDisplay];
}






#pragma mark - drawing
- (void)drawRect:(CGRect)rect
{

  //we could be adding a CALayer for each new line, which would be cheaper because you could draw each and basically forget it

  CGContextRef _context = UIGraphicsGetCurrentContext();
  CGContextSetLineWidth(_context, 1.0);  //or whatever


///older lines
  if ([_touches count]) {
  for (int line = 0; line < [_touches count]; line ++) {


    NSArray *thisLine = [_touches objectAtIndex:line];
    if ([thisLine count]) {

      CGContextSetStrokeColorWithColor(_context, [self colourForLineAtIndex:line].CGColor);
      CGPoint start = CGPointFromString([thisLine objectAtIndex:0]);
      CGContextMoveToPoint(_context, start.x, start.y);

    for (int touch = 1; touch < [thisLine count]; touch ++) {
      CGPoint pt = CGPointFromString([thisLine objectAtIndex:touch]);
      CGContextAddLineToPoint(_context, pt.x, pt.y);

    }
      CGContextStrokePath(_context);
    }

  }


  }
///current line
  if ([_currentTouch count]) {
    CGPoint start = CGPointFromString([_currentTouch objectAtIndex:0]);
    CGContextSetStrokeColorWithColor(_context, [self colourForLineAtIndex:[_touches count]].CGColor);
    CGContextMoveToPoint(_context, start.x, start.y);
    for (int touch = 1; touch < [_currentTouch count]; touch ++) {

      CGPoint touchPoint = CGPointFromString([_currentTouch objectAtIndex:touch]);
      CGContextAddLineToPoint(_context, touchPoint.x, touchPoint.y);

    }
    CGContextStrokePath(_context);
  }
}

-(UIColor *)colourForLineAtIndex:(int)lineIndex{

  return (lineIndex%2 == 0) ? [UIColor yellowColor] : [UIColor purpleColor];

  /// you might have a diff colour for each line, eg user might select a pencil from a toolbar etc
}


#pragma mark - undo mechanism
-(void)undo{

  if ([_currentTouch count]) {

    [_redoStore addObject:[NSArray arrayWithArray:_currentTouch]];
    [_currentTouch removeAllObjects];
    [self setNeedsDisplay];

  }else if ([_touches count]){

    [_redoStore addObject:[_touches lastObject]];
    [_touches removeLastObject];
    [self setNeedsDisplay];


  }else{
  //nothing left to undo
  }
}

-(void)redo{
  if ([_redoStore count]) {

    [_touches addObject:[_redoStore lastObject]];
    [_redoStore removeLastObject];
    [self setNeedsDisplay];

  }

}

@end
7
John Estropia On

First of all, since you are working with layers, I suggest give up on drawRect: and just work with CALayer transforms.

Second, in my opinion, the best way to implement undo-redo operations will always be command-based. As a very simple example, you can make separate methods for each command:

- (void)scaleLayerBy:(CGFloat)scale;
- (void)moveLayerByX:(CGFloat)x Y:(CGFloat)y;
// etc

And then each time the user makes an action, you add to an NSMutableArray the action id and the parameters:

[self.actionHistory addObject:@{ @"action": @"move", @"args": @[@10.0f, @20.0f] }];

Conversely, if the user invokes undo, remove the last object in that array.

Then when you need to reload the display, just reevaluate all the commands in the array.

[self resetLayers]; // reset CALayers to their initial state
for (NSDictionary *command in self.actionHistory) {
    NSArray *arguments = command[@"args"];
    if ([command[@"action"] isEqualToString:@"move"]) {
        [self moveLayerByX:[arguments[0] floatValue] Y:[arguments[1] floatValue]];
    }
    // else if other commands
}