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.
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
}
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