iOS多点触控绘图的撤销与重做功能实现求助
Hey there! Let's get that Apple Notes-style undo/redo functionality working for your drawing app. Based on your existing PathArray setup, we'll adjust things to handle history properly—whether you want to undo individual line segments or full strokes (just like Notes does).
Option 1: Undo/Redo Individual Line Segments
If your pathArray stores every tiny line segment drawn during touchesMoved, here's how to add undo/redo:
Step 1: Add History Arrays
First, add two new mutable arrays to your Draw class to track undo and redo history:
// In your Draw class header or class extension @property (nonatomic, strong) NSMutableArray *undoArray; @property (nonatomic, strong) NSMutableArray *redoArray;
Initialize them when your view is set up:
- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.pathArray = [NSMutableArray array]; self.undoArray = [NSMutableArray array]; self.redoArray = [NSMutableArray array]; } return self; }
Step 2: Update Drawing Logic
When you finish a segment (in touchesEnded), add it to your main array and clear the redo history (since new drawing invalidates previous redo steps):
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (!drawable) return; UITouch *touch = [touches anyObject]; CGPoint currentPoint = [touch locationInView:self]; // Create and add the new segment to your main array PathArray *newSegment = [[PathArray alloc] initWithStartPoint:lastPoint andEnd:currentPoint andColor:strokecolor andPathWidth:pathWidth]; [self.pathArray addObject:newSegment]; // Clear redo history—new drawing means we can't redo old steps anymore [self.redoArray removeAllObjects]; [self setNeedsDisplay]; [super touchesEnded:touches withEvent:event]; }
Step 3: Implement Undo/Redo Methods
Add these methods to handle the undo and redo actions:
// Undo the last drawn segment - (void)undoDrawing { if (self.pathArray.count == 0) return; // Nothing to undo PathArray *lastSegment = [self.pathArray lastObject]; [self.pathArray removeLastObject]; [self.undoArray addObject:lastSegment]; [self setNeedsDisplay]; } // Redo the last undone segment - (void)redoDrawing { if (self.undoArray.count == 0) return; // Nothing to redo PathArray *lastUndoneSegment = [self.undoArray lastObject]; [self.undoArray removeLastObject]; [self.pathArray addObject:lastUndoneSegment]; [self setNeedsDisplay]; }
Option 2: Undo/Redo Full Strokes (Apple Notes Style)
Apple Notes undoes entire strokes (from when you press the stylus down to when you lift it), not individual segments. To match this behavior, we'll adjust your data structure to group segments into full strokes:
Step 1: Create a Stroke Class
First, make a new Stroke class to hold all segments of a single stroke:
// Stroke.h #import <Foundation/Foundation.h> #import "PathArray.h" @interface Stroke : NSObject @property (nonatomic, strong) NSMutableArray<PathArray *> *segments; @property (nonatomic, strong) UIColor *color; @property (nonatomic, assign) CGFloat pathWidth; @end // Stroke.m #import "Stroke.h" @implementation Stroke - (instancetype)init { self = [super init]; if (self) { self.segments = [NSMutableArray array]; } return self; } @end
Step 2: Update Draw Class Arrays
Replace your pathArray with stroke-specific arrays, plus a reference to the current active stroke:
// In your Draw class @property (nonatomic, strong) NSMutableArray<Stroke *> *strokeArray; @property (nonatomic, strong) NSMutableArray<Stroke *> *undoStrokeArray; @property (nonatomic, strong) NSMutableArray<Stroke *> *redoStrokeArray; @property (nonatomic, strong) Stroke *currentStroke; // Track the stroke being drawn
Initialize them:
- (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.strokeArray = [NSMutableArray array]; self.undoStrokeArray = [NSMutableArray array]; self.redoStrokeArray = [NSMutableArray array]; } return self; }
Step 3: Update Touch Handling
Adjust your touch methods to build full strokes:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch* touch = [touches anyObject]; if ([touch type] == UITouchTypeStylus) { if(drawable){ [self.delegate changeScrollViewInteraction:NO]; lastPoint = [touch locationInView:self]; // Start a new stroke self.currentStroke = [[Stroke alloc] init]; self.currentStroke.color = strokecolor; self.currentStroke.pathWidth = pathWidth; } } else { [self.delegate changeScrollViewInteraction:YES]; return; } [super touchesBegan:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { if (!drawable || !self.currentStroke) return; UITouch *touch = [touches anyObject]; CGPoint currentPoint = [touch locationInView:self]; // Add a segment to the current stroke PathArray *segment = [[PathArray alloc] initWithStartPoint:lastPoint andEnd:currentPoint andColor:self.currentStroke.color andPathWidth:self.currentStroke.pathWidth]; [self.currentStroke.segments addObject:segment]; lastPoint = currentPoint; [self setNeedsDisplay]; [super touchesMoved:touches withEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (!drawable || !self.currentStroke) return; // Add the completed stroke to the main array [self.strokeArray addObject:self.currentStroke]; self.currentStroke = nil; // Clear redo history [self.redoStrokeArray removeAllObjects]; [self setNeedsDisplay]; [super touchesEnded:touches withEvent:event]; }
Step 4: Update drawRect
Modify your drawing method to render full strokes:
- (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetLineCap(context, kCGLineCapRound); for (Stroke *stroke in self.strokeArray) { // Set stroke properties once per full stroke CGContextSetStrokeColorWithColor(context, stroke.color.CGColor); CGContextSetLineWidth(context, stroke.pathWidth); // Draw all segments in the stroke for (PathArray *segment in stroke.segments) { CGContextBeginPath(context); CGContextMoveToPoint(context, segment.start.x, segment.start.y); CGContextAddLineToPoint(context, segment.end.x, segment.end.y); CGContextStrokePath(context); } } }
Step 5: Implement Stroke-Based Undo/Redo
Add methods to undo/redo entire strokes:
- (void)undoDrawing { if (self.strokeArray.count == 0) return; Stroke *lastStroke = [self.strokeArray lastObject]; [self.strokeArray removeLastObject]; [self.undoStrokeArray addObject:lastStroke]; [self setNeedsDisplay]; } - (void)redoDrawing { if (self.undoStrokeArray.count == 0) return; Stroke *lastUndoneStroke = [self.undoStrokeArray lastObject]; [self.undoStrokeArray removeLastObject]; [self.strokeArray addObject:lastUndoneStroke]; [self setNeedsDisplay]; }
Final Notes
- You'll need to hook up these
undoDrawingandredoDrawingmethods to UI controls (like buttons or gesture recognizers) to let users trigger the actions. - Both options follow Apple Notes' behavior: if you undo a few steps and then draw something new, the redo history gets cleared (you can't redo steps that are now irrelevant to the current drawing).
内容的提问来源于stack exchange,提问作者Kiet Nguyen

