I wrote a simple drawing code using UIBezierPath and the -touches... methods of UIView. The drawing works great, but I made some bad experiences.
When I'm painting very slow, it works great. But the more fast I draw, the more edgy the line gets. So how do I like "smooth" them to become less edgy (more points)?
When I use setLineWidth with high line widths, the line become very ugly.
Here is an image to show what ugly actually means:
Why does it draw fat line in that way?!
EDIT: here some code
- (void)drawInRect:(CGRect)rect
{
for(UIBezierPath *path in pathArray) {
[[UIColor blackColor] setStroke];
[path setLineWidth:50.0];
[path stroke];
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
startPoint = [[touches anyObject] locationInView:self];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:startPoint];
[path addLineToPoint:[[touches anyObject] locationInView:self]];
if(isDrawing) {
[pathArray removeLastObject];
}
else {
isDrawing = YES;
}
[pathArray addObject:path];
[path close];
[self setNeedsDisplay];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
isDrawing = NO;
}
I hope that somebody can help me with these issues. THANKS a lot, greets, Julian
Mmmmh, I'm not going to address performance issues with your implementation in this thread, but take a look at setNeedsDisplayInRect: in UIView once you get the basics working.
I think you're basically trying to take out the last created path from your array and replace it with a new one for as long as you're moving.
You should try to put a CGMutablePathRef in your array instead (take a look here for that CGMutablePathRef to NSMutableArray).
The basic flow would be something like:
touchesBegan:
Create the CGMutablePathRef
Call moveToPoint
Store in the array
touchesMoved:
Get the lastObject from your array (i.e. the CGMutablePathRef)
Call addLineToPoint
[self setNeedsDisplay]
Then in drawInRect, iterate through your paths and paint them.
Again, this will be slow at first, then you need to optimize. CGLayerRef can help. setNeedsDisplayInRect most definitely will also.
Related
I have some troubles with drawing app in IOS. I have created the free hand drawing with the help of some tutorials. But I found some difficulties in erasing the drawing. In my app, I have button with eraser as background image. After I clicked the eraser button, when I swipes over the drawing, it will erase the drawing wherever I swipes. Can anyone help me to do this.
Thanks in advance.
Given below is my code:
#implementation LinearInterpView
{
UIBezierPath *path;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder {
if(self = [super initWithCoder:aDecoder]) {
[self setMultipleTouchEnabled:YES];
[self setBackgroundColor:[UIColor whiteColor]];
path=[UIBezierPath bezierPath];
[path setLineWidth:2.0];
}
return self;
}
-(void)drawRect:(CGRect)rect{
[[UIColor blackColor] setStroke];
[path stroke];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch=[touches anyObject];
CGPoint p=[touch locationInView:self];
[path moveToPoint:p];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch=[touches anyObject];
CGPoint p=[touch locationInView:self];
[path addLineToPoint:p];
[self setNeedsDisplay];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[self touchesMoved:touches withEvent:event];
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
[self touchesEnded:touches withEvent:event];
}
// This is the button action to erase the drawing.
- (IBAction)erase:(id)sender {
CGContextRef cgref=UIGraphicsGetCurrentContext();
CGContextSetBlendMode(cgref, kCGBlendModeClear);
}
Kindly clear me, what mistake I did.
So by drawing you mean you have drew lines on the screen say with some color you can do the same by setting white color and alpha 1 so that white lines replace the existing colored lines. A better tutorial here . This also seemed important.
First of all your logic should be make a layer on ImageView.
then you can draw on that layer then pass white color to erase.
It'll look like erase and your view will look like according to requirement.
That will surly work.
Try this to erase drawing in iOS:
- (IBAction)eraserPressed:(id)sender {
red = 255.0/255.0;
green = 255.0/255.0;
blue = 255.0/255.0;
opacity = 1.0;
}
why not you implement the same logic in the erasor button as you did in the draw button. just make the default color of the stroke in the eraser as white color or what ever color your background is.
I am using the following code to draw a UIBezierPath on a UIImageView in a UIView.
But it is not showing a green path.
- (void)drawRect:(CGRect)rect
{
[[UIColor blackColor] setStroke];
[aPath strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
self->aPath = [[UIBezierPath alloc]init];
CAShapeLayer* greenPath = [CAShapeLayer layer];
greenPath.path = aPath.CGPath;
[greenPath setFillColor:[UIColor greenColor].CGColor];
[greenPath setStrokeColor:[UIColor blueColor].CGColor];
greenPath.frame=CGRectMake(0, 0,100,30);
//add shape layer to view's layer
[[imgView layer] addSublayer:greenPath];
aPath.lineCapStyle=kCGLineCapRound;
aPath.miterLimit=0;
aPath.lineWidth=10;
[aPath moveToPoint:[mytouch locationInView:imgView]];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
[aPath addLineToPoint:[mytouch locationInView:imgView]];
[imgView setNeedsDisplay];
}
I need to show a green line wherever the UIBezierPath is drawn.
There's a lot of weird stuff going on in your code, but it runs, the problem is here
[imgView setNeedsDisplay];
As I see in your code, you want it to work on drawRect, but drawRect will only get called on setNeedsDisplay on the current view, not on imgView, switch it for
[self setNeedsDisplay];
And it will work. You'd probably need to subclass UIImageView to handle drawRect inside of it. Also, I'm guessing that you want to actually modify the image, hence the attempt to draw within in, try reading on working with CoreGraphics in an image context.
About the other issues
The main problem as I see it, is that you're confusing CALayers and CoreGraphics, as it stands right now, the CALayer you're adding to the imageView is completely unused. Just with the drawRect and setting the BezierPath as you move around should work.
Also, be careful when using ->, understand what you're getting yourself into, I usually suggest sticking to #property, and using self.myVar and _myVar.
I am trying to draw multiple UIBezierPaths on a custom view, with the ability to manipulate them individually.
path and the NSMutableArray to store the paths are instance variables declared like:
#interface MyCustomView : UIView {
UIBezierPath *path;
NSMutableArray *paths; // this is initialized in the init method
}
The path is initialized in touchesBegan as follows:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
path = [[UIBezierPath alloc] init];
[path moveToPoint:[touch locationInView:self];
}
It is moved in the touchesMoved method as follows:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[path addLineToPath:[touch locationInView:self]];
[self setsNeedsDisplay];
}
And I want to store it in the NSMutableArray in touchesEnded:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[path closePath];
[paths addObject:path];
[self setNeedsDisplay];
}
The problem is, after I draw one uibezierpath, and begin to draw one after that, the one i drew first disappears. I'm not sure why this is happening. Thanks in advance!
Note: I know a possible solution is storing all the points of each uibezierpath in a NSMutableArray and redrawing it everytime drawRect is called, but I feel like that is an inefficient implementation.
This is happening because you are using a global instance path . Instead of using a global instance add your path object to mutable array and get wherever you want.
Try replacing your code something like this.
#interface MyCustomView : UIView {
NSMutableArray *paths; // this is initialized in the init method
}
The path is initialized in touchesBegan as follows:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:[touch locationInView:self];
[paths addObject:path];
}
It is moved in the touchesMoved method as follows:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UIBezierPath *path = [paths lastObject];
[path addLineToPath:[touch locationInView:self]];
[self setsNeedsDisplay];
}
And I want to store it in the NSMutableArray in touchesEnded:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UIBezierPath *path = [paths lastObject];
[path closePath];
[self setNeedsDisplay];
}
You didn't show you drawRect: method, but be aware that in your drawRect:, you need to draw all paths that you want displayed. Every time drawRect: is entered, everything you were drawing before is cleared and has to be drawn again, so just drawing the new one will just give the new one and nothing else.
I would like to implement and design a map for a building floor in my application. Before starting, I would like to have some advices.
I'm planning to use UIBezierPath to draw the shapes. Each UIBezierPath will represent a shop on my map.
Here is an illustration (map_with_UIBezierPath)
My code structure is the following: I have a UIViewController And a UiView. In the UIViewController "viewDidLoad" method, I instantiate the UIView and in the UIView "drawRect" method, I draw the shapes like following (UIBezierPathExtension inherit from UIBezierPath):
- (void)drawRect:(CGRect)rect {
context = UIGraphicsGetCurrentContext();
[[UIColor grayColor] setFill];
[[UIColor greenColor] setStroke];
UIBezierPathExtension *aPath = [[UIBezierPathExtension alloc] init];
aPath.pathId = 1;
[aPath moveToPoint:CGPointMake(227,34.25)];
[aPath addLineToPoint:CGPointMake(298.25,34.75)];
[aPath addLineToPoint:CGPointMake(298.5,82.5)];
[aPath addLineToPoint:CGPointMake(251,83)];
[aPath addLineToPoint:CGPointMake(251,67.5)];
[aPath addLineToPoint:CGPointMake(227.25,66.75)];
[aPath closePath];
aPath.lineWidth = 2;
[aPath fill];
[aPath stroke];
[paths addObject:aPath];
UIBezierPathExtension* aPath2 = [[UIBezierPathExtension alloc] init];
aPath2.pathId = 2;
[aPath2 moveToPoint:CGPointMake(251.25,90.5)];
[aPath2 addLineToPoint:CGPointMake(250.75,83.25)];
[aPath2 addLineToPoint:CGPointMake(298.5,83)];
[aPath2 addLineToPoint:CGPointMake(298.5,90.25)];
[aPath2 closePath];
aPath2.lineWidth = 2;
[aPath2 fill];
[aPath2 stroke];
[paths addObject:aPath2];
...
}
I have also implemented the pan and pinch gesture in the UIViewController.
Now, I'm asking me how I can interact with every single shapes. I would like to detect a single tap on it, change his color and display a menu like that on the selected shape.
Can someone tell me the right direction to take?
Thx in advance
You need to look for touch events (TouchesBegan, TouchesMoved, TouchesEnded, TouchesCancelled) in your view. When you get a touch, you can ask it for its location in your view. You can use this location to test whether that point is inside any of your paths, if it is, do your cool stuff.
Using your example code, here might be a rough TouchesBegan...
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint pointTouched = [touch locationInView:self];
for (UIBezierPath *path in paths) {
if ([path containsPoint:point]) {
// do something cool with your path
// or more likely, set a flag to do the cool thing when drawing
}
}
}
}
Don't forget that you should handle all the touch events and do something sensible with each. Also, the above code is multitouch capable, but you may want to only allow a single touch, in which case there are ways to eliminiate the "touches" loop.
Here's what I want to do:
I have a UIBezierPath and I want to pass it to some method for it to be drawn. Or simply draw it from the method in which it is created.
I'm not sure how to indicate which view it should be drawn in. Do all methods for drawing have to start with
- (void)drawRect:(CGRect)rect { ...} ?
can I do
- (void)drawRect:(CGRect)rect withBezierPath:(UIBezierPath*) bezierPath { ... } ??
How do I call this function, or method, from another method?
drawRect: is something that is invoked automatically when you message setNeedsDisplay or setNeedsDisplayInRect: on a view. You never call drawRect: directly.
However you are right in saying that all drawing operations are done within the drawRect: method. Typical implementation would be,
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
/* Do your drawing on `context` */
}
Since you are using UIBezierPaths, you will need to maintain an array of bezier paths that you will need to draw and then call setNeedsDisplay when something changes.
- (void)drawRect:(CGRect)rect {
for ( UIBezierPath * path in bezierPaths ) {
/* set stroke color and fill color for the path */
[path fill];
[path stroke];
}
}
where bezierPaths is an array of UIBezierPaths.
First, save your path in an ivar
#interface SomeView {
UIBezierPath * bezierPath;
}
#property(nonatomic,retain) UIBezierPath * bezierPath;
...
#end
....
- (void)someMethod {
self.bezierPath = yourBezierPath;
[self setNeedsDisplayInRect:rectToRedraw];
}
in -drawRect:
- (void)drawRect:(CGRect)rect {
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(currentContext, 3.0);
CGContextSetLineCap(currentContext, kCGLineCapRound);
CGContextSetLineJoin(currentContext, kCGLineJoinRound);
CGContextBeginPath(currentContext);
CGContextAddPath(currentContext, bezierPath.CGPath);
CGContextDrawPath(currentContext, kCGPathStroke);
}
When you need to custom your view, you can overwrite -drawRect: on the subclass:
- (void)drawRect:(CGRect)rect
{
// config your context
[bezierPath stroke];
}
Edit: directly using -stroke make code more compact.
Drawing only happens inside a method called -drawRect: (which is automatically called when a view is marked as needing display via setNeedsDisplay). So a drawRect:withBezierPath: method will never get invoked automatically. The only way it will execute is if you call it yourself.
Once you have a UIBezierPath, however, it's very easy to draw it:
- (void)drawRect:(CGRect)rect {
UIBezierPath *path = ...; // get your bezier path, perhaps from an ivar?
[path stroke];
}
There's no need to futz around with Core Graphics if all you want to do is draw a path.
you can do something like following. just define a UIColor *setStroke; in .h file and you need to set this strokeColor object before your you call [myPath strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
- (void)drawRect:(CGRect)rect
{
[strokeColor setStroke]; // this method will choose the color from the receiver color object (in this case this object is :strokeColor)
for(UIBezierPath *_path in pathArray)
[myPath strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
#pragma mark - Touch Methods
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
myPath=[[UIBezierPath alloc]init];
myPath.lineWidth = currentSliderValue;
UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
[myPath moveToPoint:[mytouch locationInView:self]];
[pathArray addObject:myPath];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
[myPath addLineToPoint:[mytouch locationInView:self]];
[self setNeedsDisplay];
}