Obtain Bezier Path of CALayer - ios

CALayer objects have a property accessibilityPath which as stated is supposedly
Returns the path of the element in screen coordinates.
Of course as expected this does not return the path of the layer.
Is there a way to access the physical path of a given CALayer that has already been created? For instance, how would you grab the path of a UIButton's layer property once the button has been initialized?
EDIT
For reference, I am trying to detect whether a rotated button contains a point. The reason for the difficulty here is due to the fact that the buttons are drawn in a curved view...
My initial approach was to create bezier slices then pass them as a property to the button to check if the path contains the point. For whatever reason, there seems to be an ugly offset from the path and the button.
They are both added to the same view and use the same coordinates / values to determine their frame, but the registered path seems to be offset to the left from the actual drawn shape from the path. Below is an image of the shapes I have drawn. The green outline is where the path is drawn (and displayed....) where the red is approximately the area which registers as inside the path...
I'm having a hard time understanding how the registered area is different.
If anyone has any ideas on why this offset would be occurring would be most appreciated.
UPDATE
Here is a snippet of me adding the shapes. self in this case is simply a UIView added to a controller. it's frame is the full size of the controller which is `{0, height_of_device - controllerHeight, width_of_device, controllerHeight}
UIBezierPath *slicePath = UIBezierPath.new;
[slicePath moveToPoint:self.archedCenterRef];
[slicePath addArcWithCenter:self.archedCenterRef radius:outerShapeDiameter/2 startAngle:shapeStartAngle endAngle:shapeEndAngle clockwise:clockwise];
[slicePath addArcWithCenter:self.archedCenterRef radius:(outerShapeDiameter/2 - self.rowHeight) startAngle:shapeEndAngle endAngle:shapeStartAngle clockwise:!clockwise];
[slicePath closePath];
CAShapeLayer *sliceShape = CAShapeLayer.new;
sliceShape.path = slicePath.CGPath;
sliceShape.fillColor = [UIColor colorWithWhite:0 alpha:.4].CGColor;
[self.layer addSublayer:sliceShape];
...
...
button.hitTestPath = slicePath;
In a separate method in my button subclass to detect if it contains the point or not: (self here is the button of course)
...
if ([self.hitTestPath containsPoint:touchPosition]) {
if (key.alpha > 0 && !key.isHidden) return YES;
else return NO;
}
else return NO;

You completely missunderstood the property, this is for assistive technology, from the docs:
Excerpt:
"The default value of this property is nil. If no path is set, the accessibility frame rectangle is used to highlight the element.
When you specify a value for this property, the assistive technology uses the path object you specify (in addition to the accessibility frame, and not in place of it) to highlight the element."
You can only get the path from a CAShapeLayer, alls other CALayers don't need to be drawn with a path at all.
Update to your update:
I think the offset is due to a missing
UIView convert(_ point: CGPoint, to view: UIView?)
The point needs to be converted to the buttons coordinate systems.

Related

ConvertRect doesn't work as expected

I'm trying to convert deep 'Subview' frame to an upper 'UIView'. I'm attaching the view hierarchy here.
Attaching illustration:
I've tried this, but the result are way off screen :
let rect = smallSubview.convert(smallSubview.frame, to: bigSuperview)
I'm trying to convert the small 'Subview' frame, to 'VideoCrop'/bigSuperView coordinate space. Any suggestions? Thank you!
Not sure, but shouldn't you be considering bounds rather than frame of your smallSubView ??
I mean :
let rect = smallSubview.convert(smallSubview.bounds, to: bigSuperview)
EDIT
I could not have answered your comment in answer hence updating my answer :)
The quick view of convert API suggests
func convert(_ rect: CGRect, to view: UIView?) -> CGRect Description
Converts a rectangle from the receiver’s coordinate system to that of
another view.
Parameters
rect A rectangle specified in the local
coordinate system (bounds) of the receiver.
view The view that is the
target of the conversion operation. If view is nil, this method
instead converts to window base coordinates. Otherwise, both view and
the receiver must belong to the same UIWindow object.
As it suggests you should be considering bounds rather than frame :)
Whats the difference between frame and bounds then ??
Bounds : Specifies the views location and size of view in its own coordinate system.
Frame: While this specifies the location and size of view in its superViews coordinate system :)
Hence bounds of any view will have its origin as (0,0) where as frame has its x and y with respect to its superview :) while height and width being same :)
Apple's convert#to is really silly.
One way to understand it:
Say you want a view named "echo" to be exactly where you are.
echo.frame = convert(bounds, to: echo.superview!)
is exactly the same as:
echo.frame = superview!.convert(frame, to: echo.superview!)
It's like ...
convert(bounds
means essentially "your frame in your superview", and that's exactly the same as
superview!.convert(frame
which also means "your frame in your superview"
You can always do either of these two things, they're identical:
convert(bounds ...
superview!.convert(frame ...

Remove line drawn on view

I am new to CoreGraphics . I am trying to create view which contains two UIImageview added in scrollview programatically. After adding it i want to connect both center with line. I have used bezier path as well as CAShapelayer. But line is drawn on UIImageview also so i want to remove line above UIImageview or send line to back to UIImageview. I have done below code.
let path: UIBezierPath = UIBezierPath()
path.moveToPoint(CGPointMake(personalProfile.center.x, personalProfile.center.y))
path.addLineToPoint(CGPointMake(vwTwo.center.x, vwTwo.center.y))
let shapeLayer: CAShapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = UIColor.blueColor().CGColor
shapeLayer.lineWidth = 3.0
shapeLayer.fillColor = UIColor.clearColor().CGColor
self.scrollView.layer.addSublayer(shapeLayer)
Please also check screenshot, i want to remove red marked portion of blue line .
You can do this simply by reducing the zPosition of your shapeLayer
This will allow the layer to be drawn underneath your two views (and far easier than trying to calculate a new start and end point of your line). If you look at the documentation for zPosition:
The default value of this property is 0. Changing the value of this property changes the the front-to-back ordering of layers onscreen. Higher values place this layer visually closer to the viewer than layers with lower values. This can affect the visibility of layers whose frame rectangles overlap.
Therefore, as it defaults to 0, and UIViews are just wrappers for CALayers, you can use a value of -1 on your shapeLayer in order to have it drawn behind your other views.
For example:
shapeLayer.zPosition = -1
Side Note
Most of the time in Swift, you don't need to explicitly supply a type when defining a variable. You can just let Swift infer it. For example:
let path = UIBezierPath()
I would see 2 options, an easy and a harder option.
Move the UIImageView to the front after drawing the line, effectively hiding the line behind the UIImageView.
Calculate the points at which you want the line to start and end and draw a line from these points instead of the centers.

How can I add sizing handles to a UIView?

I'm trying to dynamically create views (UIImageView and UITextView) at runtime by user request and then allow the user to move and resize them. I've got everything working great, except for the resizing. I tried using the pinch gesture recognizer, but find it too clumsy for what I want. Therefore, I would like to use sizing handles. I believe I could put a pan gesture recognizer on each handle, and adjust the view frame as one of them is moved.
The problem is, I'm not quite sure how to create the sizing handles. I would indicate all the things I've tried, but truthfully, I'm not quite sure where to start. I do have a few ideas...
1) Possibly use coregraphics to draw boxes or circles on the corners and sides? Would I create a new layer and draw them on that? Not sure.
2) Stick a little image of a box or circle on each corner?
3) XIB file with the handles already placed on it?
Any suggestions appreciated. I just need to be pointed in the right direction.
Edit: Something like what Apple uses in Pages would be perfect!
First, I suggest create a custom View subclass to UIView, you will handle all of the behavior here. Let's call it ResizableView.
In the custom view, You need to draw layer or view for these dot at corner and add PangestureRecognized to them.Then you can track the location of these dot using recognizer.locationInView() when user drag them, which you will use to calculate the scale of View.Here is the code you can refer to:
func rotateViewPanGesture(recognizer: UIPanGestureRecognizer) {
touchLocation = recognizer.locationInView(self.superview)
let center = CalculateFunctions.CGRectGetCenter(self.frame)
switch recognizer.state {
case .Began:
initialBounds = self.bounds
initialDistance = CalculateFunctions.CGpointGetDistance(center, point2: touchLocation!)
case .Changed:
//Finding scale between current touchPoint and previous touchPoint
let scale = sqrtf(Float(CalculateFunctions.CGpointGetDistance(center, point2: touchLocation!)) / Float(initialDistance!))
let scaleRect = CalculateFunctions.CGRectScale(initialBounds!, wScale: CGFloat(scale), hScale: CGFloat(scale))
self.bounds = scaleRect
self.refresh()
case:.Ended:
self.refresh()
default:break
Step by step
touchLocation location of the Pangesture
center is the center of ResizableView
initialBounds is the initial bounds of the ResizableView when PanGesture begin.
initailDistance is the distance between the center of the ResizableView of touchPoint of the dot the user is dragging.
Then you can calculate the scale given initialDistance, center, touch location
Now you have scaled the view as You want. You also need to refresh the position of these dot at corner accordingly, that's what refresh() for, you need to implement it yourself.
CalculateFunctions
I tend to define some helpFunctions to help me calculate.
CalculateFunctions.CGPointGetDistance is used to calculate the distance between center of the view and touch location.
CalculateFunctions.CGRectScale return the scaled CGRect given the the scale you just calculated.
CalculateFunctions.CGRectGetCenter return the center point of a CGRect
That's just a rough idea. Actually there are many Libraries you can refer to.
Some suggestions:
SPUserResizableView
This is a ResizableView exactly what you want, but it was written in ObjC and hasn't been updated for a long time. But you can refer to it.
JLStickerTextView This may not fit your requirement very well as it is for text(edit, resize, rotate with one finger) However, this one is written in Swift, a good example for you.
If you have any questions, feel free to post it.Good Luck:-)

Ios Swift Animate a view in non linear path

I am trying to animate a UIView through non linear path(i'm not trying to draw the path itself) like this :
The initial position of the view is determinated using a trailing and bottom constraint (viewBottomConstraint.constant == 100 & viewTrailingConstraint.constant == 300)
I am using UIView.animatedWithDuration like this :
viewTrailingConstraint.constant = 20
viewBottomConstraint.constant = 450
UIView.animateWithDuration(1.5,animation:{
self.view.layoutIfNeeded()
},completition:nil)
But it animate the view in a linear path.
You can use keyFrame animation with path
let keyFrameAnimation = CAKeyframeAnimation(keyPath:"position")
let mutablePath = CGPathCreateMutable()
CGPathMoveToPoint(mutablePath, nil,50,200)
CGPathAddQuadCurveToPoint(mutablePath, nil,150,100, 250, 200)
keyFrameAnimation.path = mutablePath
keyFrameAnimation.duration = 2.0
keyFrameAnimation.fillMode = kCAFillModeForwards
keyFrameAnimation.removedOnCompletion = false
self.label.layer.addAnimation(keyFrameAnimation, forKey: "animation")
Gif
About this function
void CGContextAddQuadCurveToPoint (
CGContextRef _Nullable c,
CGFloat cpx,
CGFloat cpy,
CGFloat x,
CGFloat y
);
(cpx,cpy) is control point,and (x,y) is end point
Leo's answer of using Core Animation and CAKeyframeAnimation is good, but it operates on the view's "presentation layer" and only creates the appearance of moving the view to a new location. You'll need to add extra code to actually move the view to it's final location after the animation completes. Plus Core Animation is complex and confusing.
I'd recommend using the UIView method
animateKeyframesWithDuration:delay:options:animations:completion:. You'd probably want to use the option value UIViewKeyframeAnimationOptionCalculationModeCubic, which causes the object to move along a curved path that passes through all of your points.
You call that on your view, and then in the animation block, you make multiple calls to addKeyframeWithRelativeStartTime:relativeDuration:animations: that move your view to points along your curve.
I have a sample project on github that shows this and other techniques. It's called KeyframeViewAnimations (link)
Edit:
(Note that UIView animations like animateKeyframes(withDuration:delay:options:animations:completion:) don't actually animate your views along the path you specify. They use a presentation layer just like CALayer animations do, and while the presentation layer makes the view look like it's moving along the specified path, it actually snaps from the beginning position to the end position at the beginning of the animation. UIView animations do move the view to its destination position, where CALayer animations move the presentation layer while not moving the layer/view at all.)
Another subtle difference between Leo's path-based UIView animation and my answer using UIView animateKeyframes(withDuration:delay:options:animations:completion:)is that CGPath curves are cubic or quadratic Bezier curves, and my answer animates using a different kind of curve called a Katmull-Rom spline. Bezier paths start and end at their beginning and ending points, and are attracted to, but don't pass through their middle control points. Catmull-Rom splines generate a curve that passes through every one of their control points.

iOS CALayer and TapGestureRecognizer

I'm developing an app on iOS 6.1 for iPad.
I've a problem with the CALayer and a TapGestureRecognizer.
I've 7 CALayers forming a rainbow (every layer is a colour).
Every layer is built using a CAShapeLayer generate from a CGMutablePathRef. Everything works fine. All the layers are drawn on screen and I can see a beautiful rainbow.
Now I want to detect the tap above a single color/layer. I try this way:
- (void)tap:(UITapGestureRecognizer*)recognizer
{
//I've had the tapGestureRecognizer to rainbowView (that is an UIView) in viewDidLoad
CGLayer* tappedLayer = [rainbowView.layer.presentationlayer hitTest:[recognizer locationInView:rainbowView];
if (tappedLayer == purpleLayer) //for example
NSLog(#"PURPLE!");
}
I don't understand why this code won't work! I've already red other topics in here: all suggest the hitTest: method for solving problems like this. But in my case I can't obtain the desired result.
Can anyone help me? Thanks!!
EDIT:
Here's the code for the creation of paths and layers:
- (void)viewDidLoad
{
//Other layers
...
...
//Purple Arc
purplePath = CGPathCreateMutable();
CGPathMoveToPoint(purplePath, NULL, 150, 400);
CGPathAddCurveToPoint(purplePath, NULL, 150, 162, 550, 162, 550, 400);
purpletrack = [CAShapeLayer layer];
purpletrack.path = purplePath;
purpletrack.strokeColor = [UIColor colorWithRed:134.0/255.0f green:50.0/255.0f blue:140.0/255.0f alpha:1.0].CGColor;
purpletrack.fillColor = nil;
purpletrack.lineWidth = 25.0;
[rainbowView.layer insertSublayer:purpletrack above:bluetrack];
}
This was my first approach to the problem. And the touch didn't work.
I also tried to create a RainbowView class where the rainbow was drawing in drawRect method using UIBezierPaths.
Then I follow the "Doing Hit-Detection on a Path" section in http://developer.apple.com/library/ios/#documentation/2ddrawing/conceptual/drawingprintingios/BezierPaths/BezierPaths.html
In this case the problem was the path variable passed to the method. I try to compare the UIBezierPath passed with the paths in RainbowView but with no results.
I could try to create curves instead of paths. In this case maybe there isn't a fill part of figure and the touching area is limited to the stroke. But then how can I recognize the touch on a curve?
I'm so confused about all of these stuff!!! :D
The problem you are facing is that you are checking agains the frame/bounds of the layer when hit testing and not agains the path of the shape layer.
If your paths are filled you should instead use CGPathContainsPoint() to determine if the tap was inside the path. If your paths aren't filled but instead stroked I refer you to Ole Begemann's article about CGPath Hit Testing.
To make your code cleaner you could do the hit testing in your own subclass. Also, unless the layer is animating when hit testing it makes no sense using the presentationLayer.

Resources