Question

I want to perform undo operations on the graphics context in my drawing app. When undo is pressed i want to move to the previous context that contains the old drawing.

For Example:

I have a rectangle in the context. On dragging, I move the rectangle to a new position and redraw it. Now when I press the undo button I want to move the rectangle to the previous position. How can i do this?

I have just basic idea about NSUndoManager.

Please Help!

Thanks.

Was it helpful?

Solution

The functionality you're inferring doesn't really exist. Put differently "Graphics contexts don't work this way (for free)." You may have seen the functions UIGraphicsPushContext and UIGraphicsPopContext, but they don't do what you are talking about here. The "push" and "pop" operations they represent operate on the not-yet-rendered aspects of the graphics context: the current fill and stroke colors, the clip rect, and the transform matrix for example. Once something is rendered into the context -- i.e. a path/rect/etc. has been stroked or filled, or an image has been composited into the context -- it's rendered forever. The only way to "go back" is to somehow re-create the previous state. Unfortunately, there is no magic built-in to UIGraphicsContext that will make that happen for you.

There are several ways to go about this. As another user mentioned, you can capture the state of the context after every user action. In short, this means that your document "model" is a bitmap stack. This will get very memory intensive very quickly -- bitmaps are memory-heavy. There are certainly optimizations you can make to this approach to get more mileage out of it, like only saving the region that has changed between each frame, compressing the bitmaps, swapping them out to disk, etc. There are real, usable apps in the App Store that work using this approach, but the approach is inherently limited, and you will end up expending non-trivial effort in optimizing and managing your saved stack of undo states.

There are, naturally, other approaches worth considering. The simplest is to have your actual document model be a stack of small (i.e. non-bitmap) data structures that describe the operations necessary to recreate the state of the context. When the user undoes, you simply remove the operation at the top of the stack and recreate the graphics context by playing back the remaining stack. This is a decent approach for "additive" type applications (think "Brushes") but begins to fall down even in the simple scenario you describe of moving a shape on a canvas. It also ultimately suffers from some of the same issues as the bitmap stack approach -- the more operations you have, the longer it takes to recreate the state, so you inevitably end up making bitmap snapshots periodically, etc.

For object-on-canvas scenarios like you described (the 'move shape' operation), there are also multiple approaches. One classic approach would be to have your document model be a set of smaller, more specific data structures which describe the current state of the canvas. Think of a Shape class, etc. In the simplest case, when a user adds a rectangle to the canvas, a Shape instance is added to a z-ordered array of shapes. Any time the list of shapes changes, the context gets regenerated by drawing each shape in Z order. To achieve the undo functionality, every time you mutate the array of shapes, you also use the NSUndoManager to record an invocation that, when played back, would cause the inverse operation to occur.

If your shape-add operation looked like this:

[shapeArray insertObject: newShape atIndex: 5];

Then you would, at the same time, do this with the NSUndoManager:

[[undoManager prepareInvocationWithTarget: shapeArray] removeObjectAtIndex: 5];

When the user clicks undo, the NSUndoManager plays back that invocation, and the shapeArray is returned its prior state. This is the classic NSUndoManager pattern. It works well, but also has some disadvantages. For instance, it's not necessarily straightforward to persist the undo stack across app terminations. Since app terminations are commonplace on iOS, and users generally expect an app to seamlessly restore state across terminations, an approach in which your undo stack won't survive app terminations may be a non-starter, depending on your requirements. There are other, more complex approaches, but they are mostly just variations on one of these themes. One classic worth reading about is the Command Pattern from the Gang of Four book, Design Patterns.

No matter what approach you choose, this sort of application will be a challenge to develop. This graphic undo functionality is simply not something that's built into UIGraphicsContext for "free". You've asked several times in the comments for an example. I'm sorry to say, but this is a complex enough concept that it's unlikely to be feasible for someone to provide a working example within the limitations of a StackOverflow answer. Hopefully these ideas and pointers are helpful. There are also certainly any number of open source drawing applications that you could look at for inspiration (although I'm not personally aware of any open source drawing applications for iOS.)

OTHER TIPS

UIGraphicsContext doesn't have it's own undo stack. You need to store each element of what you're drawing on a stack, and remove and add items from that stack to undo and redo. The NSUndoManager class can assist you with managing the logic for the undo and redo operations themselves, but its your responsibility to write the code that saves drawing actions to a stack and then reads from it to recreate the drawing in -drawRect:.

First set the undomanager object and initialise it for usage.

NSUndoManager *undoObject;
undoObject =[[NSUndoManager alloc] init];

Register the undo object with the target function for saving the uigraphics context, every time the context is changed.

[[undoObject prepareWithInvocationTarget:self] performUndoWithObject:currentContext withLastpoint:lastPoint andFirstPoint:firstPoint];

Write the userDefined function definition

-(void)performUndoWithObject:(CGContextRef )context withLastpoint:(CGPoint)previousLastPoint andFirstPoint:(CGPoint)previousFirstPoint
{
    // necessary steps for undoing operation
}

Specify action when an undo button is clicked.

-(void)undoButtonClicked
{
    if([undoObject canUndo])
    {
        [undoObject undo];
    }   
}

How to perform undo and redo operation in objective c for store the value array in iOS. and i have create the small demo in Undo and Redo Button work in string value replaced. Create the two NSMutableArray in ViewController.h Files

@interface ViewController : UIViewController<UITextFieldDelegate>
{
     NSMutableArray *arrUndo, *arrRedo;
}
@property (weak, nonatomic) IBOutlet UIButton *btnUndo;
@property (weak, nonatomic) IBOutlet UIButton *btnRedo;
@property (weak, nonatomic) IBOutlet UITextField *txtEnterValue;
@property (weak, nonatomic) IBOutlet UILabel *lblShowUndoOrRedoValue;
@property (weak, nonatomic) IBOutlet UIButton *btnOK;
- (IBAction)btnUndo:(id)sender;
- (IBAction)btnRedo:(id)sender;
- (IBAction)btnOK:(id)sender;
@end

Implement the ViewController.m Files to add the two button action method in undo or redo.

- (void)viewDidLoad {
    [super viewDidLoad];
    self.txtEnterValue.delegate = self;
    arrUndo = [[NSMutableArray alloc] init];
    arrRedo = [[NSMutableArray alloc] init];
}
-(BOOL)canBecomeFirstResponder {
    return YES;
}
- (IBAction)btnOK:(id)sender {

    NSString *str = [NSString stringWithFormat:@"%@",    [self.txtEnterValue.text description]];
    self.txtEnterValue.text = nil;
    self.lblShowUndoOrRedoValue.text = str;
    [arrUndo addObject:str];
    NSLog(@"Text Value : %@", [arrUndo description]);
}
- (IBAction)btnUndo:(id)sender {

    if ([arrUndo lastObject] == nil) {
        NSLog(@"EMPTY");
    }
    else
    {
    [arrRedo addObject:[arrUndo lastObject]];
    NSLog(@"ArrAdd %@", [arrUndo description]);

    [arrUndo removeLastObject];
    for (NSObject *obj in arrUndo) {
        if ([arrUndo lastObject] == obj) {

            self.lblShowUndoOrRedoValue.text = obj;
        }
    }
    NSLog(@"ArrText %@", [arrUndo description]);
    }
}
- (IBAction)btnRedo:(id)sender {

    if ([arrRedo lastObject] == nil) {
        NSLog(@"EMPTY");
    }
    else
    {
    [arrUndo addObject:[arrRedo lastObject]];
    for (NSObject *obj in arrRedo) {
        if ([arrRedo lastObject] == obj) {

            self.lblShowUndoOrRedoValue.text = obj;

        }
    }
    }
    [arrRedo removeLastObject];
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top