I am working on a drawing app, I want to do Undo/Redo, for this I am saving CGPath on touches ended to NSMutableArray, But I am not understanding how I should render the CGPaths on click of Undo button
EDIT1:
As I am using BezierPaths, So, I first decided to go with a simple approach of just stroking this path, without CGPath,
EDIT2: As my Undo is happening in segments(i,e in parts and not whole path is deleted), I decided to create an array of array, So I have made changes accordingly and now I will be drawing in CGlayer, with taking CGPath
So here "parentUndoArray" is array of arrays.
So I have done this way
I have a class called DrawingPath which will do the drawing
//DrawingPath.h
@interface DrawingPath : NSObject
@property (strong, nonatomic) NSString *pathWidth;
@property (strong,nonatomic) UIColor *pathColor;
@property (strong,nonatomic) UIBezierPath *path;
- (void)draw;
@end
//DrawingPath.m
#import "DrawingPath.h"
@implementation DrawingPath
@synthesize pathWidth = _pathWidth;
@synthesize pathColor = _pathColor;
@synthesize path = _path;
- (id)init {
if (!(self = [super init] ))
return nil;
_path = [[UIBezierPath alloc] init];
_path.lineCapStyle=kCGLineCapRound;
_path.lineJoinStyle=kCGLineJoinRound;
[_path setLineWidth:2.0f];
return self;
}
- (void)draw
{
[self.pathColor setStroke];
[self.path stroke];
}
So now in my DrawingView, I do it this way
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
ctr = 0;
bufIdx = 0;
UITouch *touch = [touches anyObject];
pts[0] = [touch locationInView:self];
isFirstTouchPoint = YES;
[m_undoArray removeAllObjects];//On every touches began clear undoArray
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint p = [touch locationInView:self];
ctr++;
pts[ctr] = p;
if (ctr == 4)
{
pts[3] = midPoint(pts[2], pts[4]);
for ( int i = 0; i < 4; i++)
{
pointsBuffer[bufIdx + i] = pts[i];
}
bufIdx += 4;
dispatch_async(drawingQueue, ^{
self.currentPath = [[DrawingPath alloc] init];
[self.currentPath setPathColor:self.lineColor];
if (bufIdx == 0) return;
LineSegment ls[4];
for ( int i = 0; i < bufIdx; i += 4)
{
if (isFirstTouchPoint) // ................. (3)
{
ls[0] = (LineSegment){pointsBuffer[0], pointsBuffer[0]};
[self.currentPath.path moveToPoint:ls[0].firstPoint];
// [offsetPath addLineToPoint:ls[0].firstPoint];
isFirstTouchPoint = NO;
}
else
{
ls[0] = lastSegmentOfPrev;
}
float frac1 = self.lineWidth/clamp(len_sq(pointsBuffer[i], pointsBuffer[i+1]), LOWER, UPPER); // ................. (4)
float frac2 = self.lineWidth/clamp(len_sq(pointsBuffer[i+1], pointsBuffer[i+2]), LOWER, UPPER);
float frac3 = self.lineWidth/clamp(len_sq(pointsBuffer[i+2], pointsBuffer[i+3]), LOWER, UPPER);
ls[1] = [self lineSegmentPerpendicularTo:(LineSegment){pointsBuffer[i], pointsBuffer[i+1]} ofRelativeLength:frac1]; // ................. (5)
ls[2] = [self lineSegmentPerpendicularTo:(LineSegment){pointsBuffer[i+1], pointsBuffer[i+2]} ofRelativeLength:frac2];
ls[3] = [self lineSegmentPerpendicularTo:(LineSegment){pointsBuffer[i+2], pointsBuffer[i+3]} ofRelativeLength:frac3];
[self.currentPath.path moveToPoint:ls[0].firstPoint]; // ................. (6)
[self.currentPath.path addCurveToPoint:ls[3].firstPoint controlPoint1:ls[1].firstPoint controlPoint2:ls[2].firstPoint];
[self.currentPath.path addLineToPoint:ls[3].secondPoint];
[self.currentPath.path addCurveToPoint:ls[0].secondPoint controlPoint1:ls[2].secondPoint controlPoint2:ls[1].secondPoint];
[self.currentPath.path closePath];
lastSegmentOfPrev = ls[3]; // ................. (7)
}
[m_undoArray addObject:self.currentPath];
EDIT:2
CGPathRef cgPath = self.currentPath.path.CGPath;
mutablePath = CGPathCreateMutableCopy(cgPath);
dispatch_async(dispatch_get_main_queue(), ^{
bufIdx = 0;
[self setNeedsDisplay];
});
});
pts[0] = pts[3];
pts[1] = pts[4];
ctr = 1;
}
}
}
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[parentUndoArray addObject:m_undoArray];
}
My drawRect method is below
EDIT: Now my DrawRect has two cases
- (void)drawRect:(CGRect)rect
{
switch (m_drawStep)
{
case DRAW:
{
CGContextRef context = UIGraphicsGetCurrentContext();//Get a reference to current context(The context to draw)
CGContextRef layerContext = CGLayerGetContext(self.currentDrawingLayer);
CGContextBeginPath(layerContext);
CGContextAddPath(layerContext, mutablePath);
CGContextSetStrokeColorWithColor(layerContext, self.lineColor.CGColor);
CGContextSetFillColorWithColor(layerContext, self.lineColor.CGColor);
CGContextSetBlendMode(layerContext,kCGBlendModeNormal);
CGContextDrawPath(layerContext, kCGPathFillStroke);
// CGPathRelease(mutablePath);
CGContextDrawLayerInRect(context,rectSize, self.newDrawingLayer);
CGContextDrawLayerInRect(context, self.bounds, self.permanentDrawingLayer);
CGContextDrawLayerInRect(context, self.bounds, self.currentDrawingLayer );
}
break;
case UNDO:
{
for(int i = 0; i<[m_parentUndoArray count];i++)
{
NSMutableArray *undoArray = [m_parentUndoArray objectAtIndex:i];
for(int i =0; i<[undoArray count];i++)
{
DrawingPath *drawPath = [undoArray objectAtIndex:i];
[drawPath draw];
}
}
}
break;
[super drawRect:rect];
}
EDIT2:Now the problem what I am facing now is that, even if I draw small paths or large paths, the data in the array of array is same. But infact, small path should contain lesser drawingPath object and large path should contain more drawingPath object in undoArray, which at last is added to array of array called "ParentUndoArray
Here are the the screen shots,
1.first screen Shot of drawn line at a stretch without lifting the finger
2, After doing undo operation once, only segment of that line is removed
I have found a solution for this, we need to create a array of array of DrawingPaths
:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// Do the above code, then
[m_undoArray addObject:self.currentPath];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[m_parentUndoArray addObject:[NSArray arrayWithArray:m_undoArray]];
}
and then stroke the path in DrawRect
.
If your drawRect method can draw whatever CGPaths are in the array, all you need to do is trigger the drawing again in you Undo method by calling setNeedsDisplay after you have removed the last added CGPath
I think you're making more path objects than you intend. I suggest go to where you alloc the bezierPath in touches moved and replace
self.currentPath = [[DrawingPath alloc] init];
With
if(!self.currentPath){
self.currentPath = [[DrawingPath alloc] init];
}
Well You should be doing it this way as its easy
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor clearColor];
myPath = [[UIBezierPath alloc] init];
myPath.lineCapStyle = kCGLineCapRound;
myPath.miterLimit = 0;
bSize=5;
myPath.lineWidth = bSize;
brushPattern = [UIColor whiteColor];
// Arrays for saving undo-redo steps in arrays
pathArray = [[NSMutableArray alloc] init];
bufferArray = [[NSMutableArray alloc] init];
}
return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
[brushPattern setStroke];
for (id path in pathArray){
if ([path isKindOfClass:[UIBezierPath class]]) {
UIBezierPath *_path=(UIBezierPath *)path;
[_path strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
}
}
#pragma mark - Touch Methods
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *mytouch = [[touches allObjects] objectAtIndex:0];
myPath = [[UIBezierPath alloc] init];
myPath.lineWidth = bSize;
[myPath moveToPoint:[mytouch locationInView:self]];
[pathArray addObject:myPath];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[myPath addLineToPoint:[[touches anyObject] locationInView:self]];
[self setNeedsDisplay];
}
#pragma mark - Undo Method
-(void)undoButtonClicked
{
if([pathArray count]>0)
{
if ([[pathArray lastObject] isKindOfClass:[SPUserResizableView class]])
{
[[pathArray lastObject] removeFromSuperview];
}
UIBezierPath *_path = [pathArray lastObject];
[bufferArray addObject:_path];
[pathArray removeLastObject];
[self setNeedsDisplay];
}
}
-(void)setBrushSize: (CGFloat)brushSize
{
bSize=brushSize;
}
-(void)redoButtonClicked
{
if([bufferArray count]>0){
UIBezierPath *_path = [bufferArray lastObject];
[pathArray addObject:_path];
[bufferArray removeLastObject];
[self setNeedsDisplay];
}
}
-(void)undoAllButtonClicked
{
[pathArray removeAllObjects];
[self setNeedsDisplay];
}
Hope it will help.
来源:https://stackoverflow.com/questions/21438586/undo-redo-for-drawing-in-ios