Crash using [UIBezierPath CGPath] with CAShapeLayer under ARC - ios

I'm getting a BAD ACCESS error using [UIBezierPath CGPath] with CAShapeLayer under ARC. I've tried bridging in various ways but I'm not clear if that is the problem. I have isolated the crash to using the result of the makeToPath method:
maskLayer = [CAShapeLayer layer];
maskLayer.path = [self makeToPath];
But this doesn't crash:
maskLayer = [CAShapeLayer layer];
maskLayer.path = [self makeFromPath];
Is there something invalid with the path created by makeToPath? I'm planning to use the from and to paths with a CABasicAnimation once I sort this crash out. What is the correct ARC bridging for CGPathRefs from UIBezierPath?
-(CGPathRef)makeToPath
{
UIBezierPath* triangle = [UIBezierPath bezierPath];
[triangle moveToPoint:CGPointZero];
[triangle addLineToPoint:CGPointMake(self.view.frame.size.width,0)];
[triangle addLineToPoint:CGPointMake(0, self.view.frame.size.height)];
[triangle closePath];
return [triangle CGPath];
}
-(CGPathRef)makeFromPath
{
UIBezierPath*rect = [UIBezierPath bezierPathWithRect:self.view.frame];
return [rect CGPath];
}
UPDATE So I changed my .h file per an answer below but I am still getting the crash
-(CGPathRef)makeToPath CF_RETURNS_RETAINED;
-(CGPathRef)makeFromPath CF_RETURNS_RETAINED;
I also tried making my methods return a UIBezierPath instance per the answer here (shown below). Still no success. Anyone want to give me the longform explanation on how to fix this?
maskLayer.path = [[self makeToPath] CGPath];// CRASHES
morph.toValue = CFBridgingRelease([[self makeToPath] CGPath]);// CRASHES
-(UIBezierPath*)makeToPath
{
UIBezierPath* triangle = [UIBezierPath bezierPath];
[triangle moveToPoint:CGPointZero];
[triangle addLineToPoint:CGPointMake(self.view.frame.size.width,0)];
[triangle addLineToPoint:CGPointMake(0, self.view.frame.size.height)];
[triangle closePath];
return triangle;
}

The problem is with returning the CGPath. The value returned is a CGPathRef which is not covered by ARC. The UIBezierPath you create is released after the method ends. Thus also freeing the CGPathRef.
You can specify a source annotation to let ARC know your intent:
In the .h file:
-(CGPathRef)makeToPath CF_RETURNS_RETAINED;
-(CGPathRef)makeFromPath CF_RETURNS_RETAINED;

As the other poster pointed out, you are returning a CGPath reference taken from a UIBezierPath object that goes out of scope at the end of the method. As the docs on the UIBezierPath CGPath property say:
The path object itself is owned by the UIBezierPath object and is
valid only until you make further modifications to the path.
You need to create a copy of your CGPath and return that:
-(CGPathRef)makeToPath
{
UIBezierPath* triangle = [UIBezierPath bezierPath];
[triangle moveToPoint:CGPointZero];
[triangle addLineToPoint:CGPointMake(self.view.frame.size.width,0)];
[triangle addLineToPoint:CGPointMake(0, self.view.frame.size.height)];
[triangle closePath];
CGPathRef theCGPath = [triangle CGPath];
return CGPathCreateCopy(theCGPath);
}
The way I read the link to the llvm project, I think that the cf_returns_retained qualifier is intended to tell the caller the memory management policy for the returned value, rather than doing the retain for you.
Thus I think you would both need to create a copy of the path AND add the cf_returns_retained qualifier. I'm not clear on the syntax of that qualifier, however. (Never used it before.)
Assuming the other poster had the right syntax, it would look something like this:
-(CGPathRef)makeToPath CF_RETURNS_RETAINED;
{
UIBezierPath* triangle = [UIBezierPath bezierPath];
[triangle moveToPoint:CGPointZero];
[triangle addLineToPoint:CGPointMake(self.view.frame.size.width,0)];
[triangle addLineToPoint:CGPointMake(0, self.view.frame.size.height)];
[triangle closePath];
CGPathRef theCGPath = [triangle CGPath];
return CGPathCreateCopy(theCGPath);
}

Related

Draw on UIView or GLKView?

I need to draw a separator line with some dots on it. I have decided I will do this using the draw method, as opposed to including images of the separator. I will do this for performance and for customisability, as the separator changes sometimes.
Now I have looked into the draw() method on UIView and I have noticed that Apple suggests using GLKView when drawing using OpenGL.
For a simple separator, won't it be too much of a hassle to call OpenGL? Or is the OpenGL overhead negligible? When would I want to use the native UIKit draw() then?
FYI I don't know either method, but want to learn both methods, so don't reply "what you know best". I am simply asking about performance.
OpenGL uses GPU instead of CPU for computation. If you are making something like a gaming app, then you can think of using OpenGL. I believe you want to draw a line in an iOS App. For that you can either use drawRect method in UIView or create a shapeLayer and add it as a sublayer.
The following examples will show you:
CAShapeLayer *simpleLine = [CAShapeLayer layer];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 80)];
[path addLineToPoint:CGPointMake(300, 80)];
simpleLine.lineWidth = 1.0;
simpleLine.path = path.CGPath;
simpleLine.strokeColor = [[UIColor blackColor] CGColor];
[[self.view layer] addSublayer:simpleLine];
For using drawRect, you are supposed to do this inside a Custom UIView as opposed to the above method.
- (void)drawRect:(CGRect)rect {
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 80)];
[path addLineToPoint:CGPointMake(300, 80)];
path.lineWidth = 1.0;
[[UIColor blueColor] setStroke];
[path stroke];
}
If your separator parameters changes and if you are making an app, it's better to use drawRect method. You can call this method anytime by using [CustomUIView setNeedsDisplay:YES]
Edit
What you're asking for is circle over line. You can do that by drawing UIBezierPath for line first and then add UIBezierPath for circle later.
Normal Line
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10.0, 10.0)];
[path addLineToPoint:CGPointMake(100.0, 100.0)];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = [[UIColor redColor] CGColor];
shapeLayer.lineWidth = 3.0;
shapeLayer.fillColor = [[UIColor clearColor] CGColor];
[self.view.layer addSublayer:shapeLayer];
Dotted Line
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10.0, 10.0)];
[path addLineToPoint:CGPointMake(100.0, 100.0)];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = [[UIColor redColor] CGColor];
shapeLayer.lineWidth = 3.0;
shapeLayer.fillColor = [[UIColor clearColor] CGColor];
shapeLayer.lineDashPattern = #[#4, #2];
[self.view.layer addSublayer:shapeLayer];

Draw on screen with CoreGraphics when hit NSSetUncaughtExceptionHandler

I'm trying to draw something simple on screen when NSSetUncaughtExceptionHandler gets called but nothings happen.
Is there any other way to catch an exception and quickly draw something on screen?
NSSetUncaughtExceptionHandler(&exceptionHandler);
void exceptionHandler(NSException *exception)
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10.0, 10.0)];
[path moveToPoint:CGPointMake(10.0, 10.0)];
[path addLineToPoint:CGPointMake(10.0, 450.0)];
[path addLineToPoint:CGPointMake(310.0, 450.0)];
[path addLineToPoint:CGPointMake(310.0, 10.0)];
[path addLineToPoint:CGPointMake(10.0, 10.0)];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = [[UIColor blueColor] CGColor];
shapeLayer.lineWidth = 3.0;
shapeLayer.fillColor = [[UIColor clearColor] CGColor];
[APP_DELEGATE_WINDOW.rootViewController.view.layer addSublayer:shapeLayer];
[APP_DELEGATE_WINDOW makeKeyAndVisible];
}
I'm not sure that the NSSetUncaughtExceptionHandler is a good place to draw stuff on the screen.
Sets the top-level error-handling function where you can perform
last-minute logging before the program terminates.
Most crash reporters carefully log the trace and try not to "touch" anything besides.
If you really want to draw something (what?) you should maybe catch a particular exception first. Just not any uncaught exception.
I think Rivera is right, about NSSetUncaughtExceptionHandler. And maybe you need to write: [path fill];
P.S : I'm sorry that i write here, comments disabled, small reputation...
Like Rivera said, you won't be able to rescue the application at this stage, but if you really needed to draw something here, you could probably launch another process (which you would have to design) just to show this image.

Create a UIBezierPath (or CGPath) with a Quarter-Circle Arc that's the 'Opposite' of a Rounded Corner

I've been using the following code to create views with rounded corners:
- (void)setMaskTo:(UIView*)view byRoundingCorners:(UIRectCorner)corners
{
UIBezierPath* rounded = [UIBezierPath bezierPathWithRoundedRect:view.bounds
byRoundingCorners:corners
cornerRadii:CGSizeMake(_cornerRadius, _cornerRadius)];
CAShapeLayer* shape = [[CAShapeLayer alloc] init];
[shape setPath:rounded.CGPath];
view.layer.mask = shape;
}
So I've been calling it with something like:
[self setMaskTo:someView byRoundingCorners:UIRectCornerAllCorners];
Or:
[self setMaskTo:someOtherView byRoundingCorners:UIRectCornerBottomRight];
I now want to create some views corresponding to one of the bits on the outside of a single rounded corner...
A good approach (think I) is to create the appropriate path so that I can create a CAShapeLayer and then use that as the mask for the view (similar to above).
I'm not sure what the best approach is, which methods etc., when using the UIBezierPath class (or CGPath).
Any suggestions?
Using UIViews and a mask layer, to get the shapes that I wanted, I just needed to play around creating paths with an arc...
I'm aware that the methods for working with a CGPath can be used, but I've worked with what is available with UIBezierPath.
I've used the following methods to create the paths that I need:
- (UIBezierPath*)oppositeArcOfPathForBottomRightCorner
{
float width = _cornerRadius;
UIBezierPath* path = [UIBezierPath new];
[path moveToPoint:CGPointMake(0, width)];
[path addLineToPoint:CGPointMake(width, width)];
[path addLineToPoint:CGPointMake(width, 0)];
[path addArcWithCenter:CGPointMake(0, 0) radius:width startAngle:0 endAngle:(M_PI / 2.0f) clockwise:YES];
[path closePath];
return path;
}
- (UIBezierPath*)oppositeArcOfPathForTopLeftCorner
{
float width = _cornerRadius;
UIBezierPath* path = [UIBezierPath new];
[path moveToPoint:CGPointMake(0, width)];
[path addArcWithCenter:CGPointMake(width, width) radius:width startAngle:M_PI endAngle:(M_PI * 1.5f) clockwise:YES];
[path addLineToPoint:CGPointMake(0, 0)];
[path closePath];
return path;
}
And then something like this to use as a mask with a UIView:
- (void)setMaskTo:(UIView*)view fromPath:(UIBezierPath*)path
{
CAShapeLayer* shape = [[CAShapeLayer alloc] init];
[shape setPath:path.CGPath];
view.layer.mask = shape;
}

uibezier path - will not display

I am trying to use UIbezierPaths for the first time and I am having no success in displaying it in the view. I am using a very old version of xcode (3.2.6).
-(void)drawshape:(CGRect)Rect {
UIBezierPath *cloudpath = [UIBezierPath bezierPath];
[cloudpath moveToPoint:CGPointMake(100.0, 100.0)];
[cloudpath addLineToPoint:CGPointMake(200, 200)];
[cloudpath addLineToPoint:CGPointMake(200, 300)];
[cloudpath addLineToPoint:CGPointMake(250, 330)];
[cloudpath addLineToPoint:CGPointMake(20, 400)];
[cloudpath closePath];
cloudpath.lineWidth = 2;
[[UIColor blueColor]setStroke];
[cloudpath stroke];
}
I have also Imported QuartzCore. I have only added -(void)drawshape in viewcontroller.h
When I build, i have no errors.Any help is massively welcomed
You will need to initiate the drawing from overriding drawRect: in a custom UIView and not in your view controller.

How to Animate CoreGraphics Drawing of Shape Using CAKeyframeAnimation

I am trying to animate the drawing of a UIBeizerPath (in my example a triangle) in a UIView subclass. However, the entire subview is animating instead of the shape.
Is there something I am missing with the animation?
- (void)drawRect:(CGRect)rect {
CAShapeLayer *drawLayer = [CAShapeLayer layer];
drawLayer.frame = CGRectMake(0, 0, 100, 100);
drawLayer.strokeColor = [UIColor greenColor].CGColor;
drawLayer.lineWidth = 4.0;
[self.layer addSublayer:drawLayer];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0,0)];
[path addLineToPoint:CGPointMake(50,100)];
[path addLineToPoint:CGPointMake(100,0)];
[path closePath];
CGPoint center = [self convertPoint:self.center fromView:nil];
[path applyTransform:CGAffineTransformMakeTranslation(center.x, center.y)];
[[UIColor redColor] set];
[path fill];
[[UIColor blueColor] setStroke];
path.lineWidth = 3.0f;
[path stroke];
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.duration = 4.0f;
pathAnimation.path = path.CGPath;
pathAnimation.calculationMode = kCAAnimationLinear;
[drawLayer addAnimation:pathAnimation forKey:#"position"];
}
You are creating a CAShapeLayer, but then not doing anything useful with it. Let's fix that.
Don't set up layers and animations in -drawRect:, because that's strictly meant as a time to do drawing using the CoreGraphics or UIKit APIs. Instead, you want the CAShapeLayer to draw the triangle -- that way you can animate it.
CAKeyframeAnimation.path is meant for something completely different (e.g. moving a layer along a path).
Your animation is animating the position value of the layer. No surprise that it moves the layer! You want to animate the path value instead.
The idea behind CAKeyframeAnimation is that you provide it an array of values to set the layer's property to. During the time between keyframes, it will interpolate between the two adjacent keyframes. So you need to give it several paths -- one for each side.
Interpolating arbitrary paths is difficult. CA's path interpolation works best when the paths have the same same number and kind of elements. So, we make sure all our paths have the same structure, just with some points on top of each other.
The secret to animation, and maybe to computers in general: you must be precise in explaining what you want to happen. "I want to animate the drawing of each point, so it appears to be animated" is not nearly enough information.
Here's a UIView subclass that I think does what you're asking for, or at least close. To animate, hook a button up to the -animate: action.
SPAnimatedShapeView.h:
#import <UIKit/UIKit.h>
#interface SPAnimatedShapeView : UIView
- (IBAction)animate:(id)sender;
#end
SPAnimatedShapeView.m:
#import "SPAnimatedShapeView.h"
#import <QuartzCore/QuartzCore.h>
#interface SPAnimatedShapeView ()
#property (nonatomic, retain) CAShapeLayer* shapeLayer;
#end
#implementation SPAnimatedShapeView
#synthesize shapeLayer = _shapeLayer;
- (void)dealloc
{
[_shapeLayer release];
[super dealloc];
}
- (void)layoutSubviews
{
if (!self.shapeLayer)
{
self.shapeLayer = [[[CAShapeLayer alloc] init] autorelease];
self.shapeLayer.bounds = CGRectMake(0, 0, 100, 100); // layer is 100x100 in size
self.shapeLayer.position = self.center; // and is centered in the view
self.shapeLayer.strokeColor = [UIColor blueColor].CGColor;
self.shapeLayer.fillColor = [UIColor redColor].CGColor;
self.shapeLayer.lineWidth = 3.f;
[self.layer addSublayer:self.shapeLayer];
}
}
- (IBAction)animate:(id)sender
{
UIBezierPath* path0 = [UIBezierPath bezierPath];
[path0 moveToPoint:CGPointZero];
[path0 addLineToPoint:CGPointZero];
[path0 addLineToPoint:CGPointZero];
[path0 addLineToPoint:CGPointZero];
UIBezierPath* path1 = [UIBezierPath bezierPath];
[path1 moveToPoint:CGPointZero];
[path1 addLineToPoint:CGPointMake(50,100)];
[path1 addLineToPoint:CGPointMake(50,100)];
[path1 addLineToPoint:CGPointMake(50,100)];
UIBezierPath* path2 = [UIBezierPath bezierPath];
[path2 moveToPoint:CGPointZero];
[path2 addLineToPoint:CGPointMake(50,100)];
[path2 addLineToPoint:CGPointMake(100,0)];
[path2 addLineToPoint:CGPointMake(100,0)];
UIBezierPath* path3 = [UIBezierPath bezierPath];
[path3 moveToPoint:CGPointZero];
[path3 addLineToPoint:CGPointMake(50,100)];
[path3 addLineToPoint:CGPointMake(100,0)];
[path3 addLineToPoint:CGPointZero];
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:#"path"];
animation.duration = 4.0f;
animation.values = [NSArray arrayWithObjects:(id)path0.CGPath, (id)path1.CGPath, (id)path2.CGPath, (id)path3.CGPath, nil];
[self.shapeLayer addAnimation:animation forKey:nil];
}
#end

Resources