0

Im developing a colouring app, however I cant manage to implement an UNDO button. I am unsure of the approach, I have tried implementing NSUndoManager, but I could not get it to work effectively. My approach is possibly incorrect. Im would greatly appreciate an answer that uses code, based on my example.

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

    mouseSwiped = NO;
    UITouch *touch = [touches anyObject];
    lastPoint = [touch locationInView:self.view];
}  

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

   mouseSwiped = YES;
   UITouch *touch = [touches anyObject];
   CGPoint currentPoint = [touch locationInView:self.view];

   UIGraphicsBeginImageContext(self.view.frame.size);
   [self.tempImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];

   //get the current touch point and then draw a line with CGContextAddLineToPoint from lastPoint to currentPoint. You’re right to think that this approach will produce a series of straight lines, but the lines are sufficiently short that the result looks like a nice smooth curve.
   CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
   CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);


   //Now set our brush size and opacity and brush stroke color:
   CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
   CGContextSetLineWidth(UIGraphicsGetCurrentContext(), brush );
   CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), red, green, blue, 1.0);
   CGContextSetBlendMode(UIGraphicsGetCurrentContext(),kCGBlendModeNormal);
   //Finish it off by drawing the path:
   CGContextStrokePath(UIGraphicsGetCurrentContext());

   self.tempImage.image = UIGraphicsGetImageFromCurrentImageContext();
   [self.tempImage setAlpha:opacity];
   UIGraphicsEndImageContext();

   lastPoint = currentPoint;
}

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

/*
 check if mouse was swiped. If it was, then it means touchesMoved was called and you don’t need to draw any further. However, if the mouse was not swiped, then it means user just tapped the screen to draw a single point. In that case, just draw a single point.

 */
   if(!mouseSwiped) {
       UIGraphicsBeginImageContext(self.view.frame.size);
       [self.tempImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
       CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
       CGContextSetLineWidth(UIGraphicsGetCurrentContext(), brush);
       CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), red, green, blue, opacity);
       CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
       CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
       CGContextStrokePath(UIGraphicsGetCurrentContext());
       CGContextFlush(UIGraphicsGetCurrentContext());
       self.tempImage.image = UIGraphicsGetImageFromCurrentImageContext();
       UIGraphicsEndImageContext();
   }

   UIGraphicsBeginImageContext(self.mainImage.frame.size);
   [self.mainImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width,  self.view.frame.size.height) blendMode:kCGBlendModeNormal alpha:1.0];
   [self.tempImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) blendMode:kCGBlendModeNormal alpha:opacity];

   //Once the brush stroke is done, merge the tempDrawImage with mainImage,
   self.mainImage.image = UIGraphicsGetImageFromCurrentImageContext();
   self.tempImage.image = nil;
   UIGraphicsEndImageContext();
}

There has been similar questions asked but no clear answer was given, and a few left unanswered.

* EDIT 1 based on gabbler answer *

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *mainImage;
@property (weak, nonatomic) IBOutlet UIImageView *tempImage;
@property (weak, nonatomic) IBOutlet UIImageView *palette;
@property (nonatomic) UIImage * previousImage;
@property (nonatomic,readonly) NSUndoManager * undoManager;
- (IBAction)colorPressed:(id)sender;
- (IBAction)erase:(id)sender;

- (IBAction)undo:(id)sender;
@end


@implementation ViewController

//gabbler code
- (void)setImage:(UIImage*)currentImage fromImage:(UIImage*)preImage
{
    // Prepare undo-redo
   [[self.undoManager prepareWithInvocationTarget:self] setImage:preImage fromImage:currentImage];
   self.mainImage.image = currentImage;
   self.tempImage.image = currentImage;
   self.previousImage = currentImage;
}
- (IBAction)trash:(id)sender {
     self.mainImage.image = nil;
}


- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    red = 0.0/255.0;
    green = 0.0/255.0;
    blue = 0.0/255.0;
    brush = 10.0;
    opacity = 1.0;

    //gabbler code
    self.previousImage = self.tempImage.image;
}



- (IBAction)erase:(id)sender {

    //set it to background color
    red = 255.0/255.0;
    green = 255.0/255.0;
    blue = 255.0/255.0;
    opacity = 1.0;

}
//gabbler code
- (IBAction)undo:(id)sender {
    [self.undoManager undo];
}

#pragma mark - Touches
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    mouseSwiped = NO;
    UITouch *touch = [touches anyObject];
    lastPoint = [touch locationInView:self.view];
}

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

    mouseSwiped = YES;
    UITouch *touch = [touches anyObject];
    CGPoint currentPoint = [touch locationInView:self.view];

    UIGraphicsBeginImageContext(self.view.frame.size);
    [self.tempImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];

    //get the current touch point and then draw a line with CGContextAddLineToPoint from lastPoint to currentPoint. You’re right to think that this approach will produce a series of straight lines, but the lines are sufficiently short that the result looks like a nice smooth curve.
    CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
    CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);


    //Now set our brush size and opacity and brush stroke color:
    CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
    CGContextSetLineWidth(UIGraphicsGetCurrentContext(), brush );
    CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), red, green, blue, 1.0);
    CGContextSetBlendMode(UIGraphicsGetCurrentContext(),kCGBlendModeNormal);
    //Finish it off by drawing the path:
    CGContextStrokePath(UIGraphicsGetCurrentContext());

    self.tempImage.image = UIGraphicsGetImageFromCurrentImageContext();
    [self.tempImage setAlpha:opacity];
    UIGraphicsEndImageContext();

    lastPoint = currentPoint;
}

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


    /*
     check if mouse was swiped. If it was, then it means touchesMoved was called and you don’t need to draw any further. However, if the mouse was not swiped, then it means user just tapped the screen to draw a single point. In that case, just draw a single point.

     */
    if(!mouseSwiped) {
        UIGraphicsBeginImageContext(self.view.frame.size);
        [self.tempImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
        CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
        CGContextSetLineWidth(UIGraphicsGetCurrentContext(), brush);
        CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), red, green, blue, opacity);
        CGContextMoveToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
        CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), lastPoint.x, lastPoint.y);
        CGContextStrokePath(UIGraphicsGetCurrentContext());
        CGContextFlush(UIGraphicsGetCurrentContext());
        self.tempImage.image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    }

    UIGraphicsBeginImageContext(self.mainImage.frame.size);
    [self.mainImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) blendMode:kCGBlendModeNormal alpha:1.0];
    [self.tempImage.image drawInRect:CGRectMake(0, 0, self.view.frame.size.width,    self.view.frame.size.height) blendMode:kCGBlendModeNormal alpha:opacity];

    //Once the brush stroke is done, merge the tempDrawImage with mainImage,
    self.mainImage.image = UIGraphicsGetImageFromCurrentImageContext();
    self.tempImage.image = nil;
    UIGraphicsEndImageContext();

    //gabbler code
    UIImage *currentImage = UIGraphicsGetImageFromCurrentImageContext();
    [self setImage:currentImage fromImage:currentImage];
 }
 @end
DevC
  • 6,982
  • 9
  • 45
  • 80
  • 1
    I suggest to go over this https://github.com/acerbetti/ACEDrawingView and please post if you have any questions. This has implementation of undo and redo as well. – Vig Nov 17 '14 at 20:09

1 Answers1

4

UIViewController's superclass UIResponder has a NSUndoManager property, which can be used here. The above code performs a setting image action for the imageView, to undo the action, you have to keep a reference of the previous image that was set to the imageView. You can make an instance variable called previousImage, in viewDidLoad, set

previousImage = self.tempImage.image;

And here is the function to set image for UIImageView.

- (void)setImage:(UIImage*)currentImage fromImage:(UIImage*)preImage
{
    // Prepare undo-redo
    [[self.undoManager prepareWithInvocationTarget:self] setImage:preImage fromImage:currentImage];
    self.mainImage.image = currentImage;
    self.tempImage.image = currentImage;
    previousImage = currentImage;
}

In touchesEnded.

UIImage *currentImage = UIGraphicsGetImageFromCurrentImageContext();
[self setImage:currentImage fromImage:previousImage];

When you click a button for undoing.

- (IBAction)btnClicked:(id)sender {
    [self.undoManager undo];
}

Edit

The above method will cause memory issues, don't save images in undoManager, save the line path points instead, here is a sample project with undo and redo capabilities.

gabbler
  • 13,626
  • 4
  • 32
  • 44
  • Where in `touches ended` do I place the code? Putting it at the bottom results in the line getting erased immediately. Please see the above edit. – DevC Nov 18 '14 at 10:19
  • `UIImage *currentImage = UIGraphicsGetImageFromCurrentImageContext();` This line should be placed before `UIGraphicsEndImageContext()`. – gabbler Nov 18 '14 at 10:38
  • Delete this line:`@property (nonatomic,readonly) NSUndoManager * undoManager`, `UIViewController` has a built in `NSUndoManager` object. – gabbler Nov 18 '14 at 13:35
  • I noticed one issue with this. If you use multiple colors then the colors do not persist. For example, if I draw a black line, then a red, and undo the red line, the black line is now red? – DevC Nov 19 '14 at 20:19
  • 1
    The current implementation is when you undo, it redraws all existing lines with the same color, you can add an array with color info in it, for instance, array[0] = black, array[1] = red, which means line 0's color is black, line 1's color is red,when you redraw, get the corresponding color from the array. – gabbler Nov 20 '14 at 00:01
  • sorry i can give u only one up vote if i can give you unlimeted votes i will surely give you. Your sample worked out for me after some modifications according to my req thank you so much dude – ashokdy Dec 24 '15 at 09:22