Collision bounding path origin in UIView iOS 9 - ios

I'm trying to setup the collison bounding path of a custom UIView. What I currently do is converting a SVG file to a path using PocketSVG and creating an UIBezierPath from it. Here's my code:
#implementation CustomView {
UIBezierPath * _mask;
CGPathRef _myPath;
}
- (UIDynamicItemCollisionBoundsType) collisionBoundsType {
return UIDynamicItemCollisionBoundsTypePath;
}
- (UIBezierPath *) collisionBoundingPath {
_myPath = [PocketSVG pathFromSVGFileNamed:#"line5"];
_mask = [UIBezierPath bezierPathWithCGPath:_myPath];
return _mask;
}
It works "correctly" but the UIBezierPath is drawn at the center of my view so this is what I get:
Left: Actual behavior ---> Right: Expected behavior
And since the collision bounds are invisible the collision seems to happen before touching the view visually (they are actually touching because of the origin of the collision bounds path).
According to Apple documentation regarding collisionBoundingPath:
The path object you create must represent a convex polygon with
counter-clockwise or clockwise winding, and the path must not
intersect itself. The (0, 0) point of the path must be located at the
center point of the corresponding dynamic item. If the center point
does not match the path’s origin, collision behaviors may not work as
expected.
So my main question is, since the (0,0) coordinates of the path must be at the center of my view, how can I achieve what I'm looking for? The perfect scenario would be if UIBezierPath start drawing at the origin of its UIView but since I'm adding a path from a SVG it is automatically draw from the center of the view.

You can use a transformation to move the CGPath into the appropriate position. For example:
- (UIBezierPath *) collisionBoundingPath {
_myPath = [PocketSVG pathFromSVGFileNamed:#"line5"];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-self.bounds.size.width * 0.5,-self.bounds.size.height * 0.5);
CGPathRef movedPath = CGPathCreateCopyByTransformingPath(_myPath,&translation);
_mask = [UIBezierPath bezierPathWithCGPath:movedPath];
return _mask;
}

Related

Drag and sacale a rectangle drawn using UIBezierPath IOS/Swift

I am developing a program which identifies a rectangle in an image and draws a path on the border of that identified rectangle. Now I want to reposition that path in case if it is not on the exact position. For an example look at this image
In cases like this i need to drag the corners of the path and reposition it as it fits the rectangle.
To draw the path I used CAShapeLayer and UIBezierPath. Here is the code I used to draw the path.
// imgView is the UIImageView which contains the image with the rectangle
let line: CAShapeLayer = CAShapeLayer();
line.frame = imgView.bounds;
let linePath: UIBezierPath = UIBezierPath();
linePath.moveToPoint(CGPointMake(x1, y1);
linePath.addLineToPoint(CGPointMake(x2, y2);
linePath.addLineToPoint(CGPointMake(x3, y3);
linePath.addLineToPoint(CGPointMake(x4, y4);
linePath.addLineToPoint(CGPointMake(x1, y1);
linePath.closePath();
line.lineWidth = 5.0;
line.path = linePath.CGPath;
line.fillColor = UIColor.clearColor().CGColor;
line.strokeColor = UIColor.blueColor().CGColor;
imgView.layer.addSublayer(line);
The thing is I tried to add a gesture to UIBezierPath. But there is nothing like that as I noticed. Couldn't find anything regarding this. So can someone please let me know a way to get my work done. Any help would be highly appreciated.
You are right that there is no way to attach a gesture recognizer to a UIBezierPath. Gesture recognizers attach to UIView objects, and a UIBezierPath is not a view object.
There is no built-in mechanism to do this. You need to do it yourself. I would suggest building a family of classes to handle it. Create a rectangle view class. It would use a bezier path internally, as well as placing 4 corner point views on the vertexes and installing pan gesture recognizers each corner point view.
Note that Cocoa rectangles (CGRects) can't be rotated. You'll need to use a series of connected line segments and write logic that forces it to stay square.

CALayer flattening sublayers

I am creating a UI where we have a deck of cards that you can swipe off the screen.
What I had hoped to be able to do was create a subclass of UIView which would represent each card and then to modify the transform property to move them back (z-axis) and a little up (y-axis) to get the look of a deck of cards.
Reading up on it I found I needed to use a CATransformLayer instead of the normal CALayer in order for the z-axis to not get flattened. I prototyped this by creating a CATransformLayer which I added to the CardDeckView's layer, and then all my cards are added to that CATransformLayer. The code looks a little bit like this:
In init:
// Initialize the CATransformSublayer
_rootTransformLayer = [self constructRootTransformLayer];
[self.layer addSublayer:_rootTransformLayer];
constructRootTransformLayer (the angle method is redundant, was going to angle the deck but later decided not to):
CATransformLayer* transformLayer = [CATransformLayer layer];
transformLayer.frame = self.bounds;
// Angle the transform layer so we an see all of the cards
CATransform3D rootRotateTransform = [self transformWithZRotation:0.0];
transformLayer.transform = rootRotateTransform;
return transformLayer;
Then the code to add the cards looks like:
// Set up a CardView as a wrapper for the contentView
RVCardView* cardView = [[RVCardView alloc] initWithContentView:contentView];
cardView.layer.cornerRadius = 6.0;
if (cardView != nil) {
[_cardArray addObject:cardView];
//[self addSubview:cardView];
[_rootTransformLayer addSublayer:cardView.layer];
[self setNeedsLayout];
}
Note that what I originally wanted was to simply add the RVCardView directly as a subview - I want to preserve touch events which adding just the layer doesn't do. Unfortunately what ends up happening is the following:
If I add the cards to the rootTransformLayer I end up with the right look which is:
Note that I tried using the layerClass on the root view (CardDeckView) which looks like this:
+ (Class) layerClass
{
return [CATransformLayer class];
}
I've confirmed that the root layer type is now CATransformLayer but I still get the flattened look. What else do I need to do in order to prevent the flattening?
When you use views, you see a flat scene because there is no perspective set in place. To make a comparison with 3D graphics, like OpenGL, in order to render a scene you must set the camera matrix, the one that transforms the 3D world into a 2D image.
This is the same: sublayers content are transformed using CATransform3D in 3D space but then, when the parent CALayer displays them, by default it projects them on x and y ignoring the z coordinate.
See Adding Perspective to Your Animations on Apple documentation. This is the code you are missing:
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / eyePosition; // ...on the z axis
myParentDeckView.layer.sublayerTransform = perspective;
Note that for this, you don't need to use CATransformLayer, a simple CALayer would suffice:
here is the transformation applied to the subviews in the picture (eyePosition = -0.1):
// (from ViewController-viewDidLoad)
for (UIView *v in self.view.subviews) {
CGFloat dz = (float)(arc4random() % self.view.subviews.count);
CATransform3D t = CATransform3DRotate(CATransform3DMakeTranslation(0.f, 0.f, dz),
0.02,
1.0, 0.0, 0.0);
v.layer.transform = t;
}
The reason for using CATransformLayer is pointed out in this question. CALayer "rasterizes" its transformed sublayers and then applies its own transformation, while CATransformLayer preserves the full hierarchy and draws each sublayer independently; it is useful only if you have more than one level of 3D-transformed sublayers. In your case, the scene tree has only one level: the deck view (which itself has the identity matrix as transformation) and the card views, the children (which are instead moved in the 3D space). So CATransformLayer is superfluous in this case.

Drawing a Hexagon with UIBezierPath (point first)

I am trying to draw a Hexagon using UIBezierPath and ZEPolygon, it works great but my hexagon is flat on top. I have tried everything to get it to draw form a point in the middle including a 180 degree transform on the path which work but everything else break.
This is how it looks now
This is how i would like it to look
My code is below
UIImageView *maskedImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"image.png"]];
UIBezierPath *nonagon = [UIBezierPath bezierPathWithPolygonInRect:maskedImageView.frame numberOfSides:6];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = nonagon.CGPath;
maskedImageView.layer.mask = shapeLayer;
[self.view addSubview:maskedImageView];
This is the library i used for the drawing the bezier path
Thanks for any help
When you rotate a UIBezierPath with a CGTransform, it will be rotated around the point (0,0), and for your path the point (0,0) is the top left corner of your shape. This is why the offset is incorrect when you just rotate by 90 degrees w/o doing anything else - its rotating around the wrong point.
So before you rotate, you need to center the path to the point (0,0), then rotate it, then move it back so that (0,0) is at its top left.
the following code will rotate the polygon 90 degrees:
// get the size of the image, we'll need this for our path and for later too
CGRect boundsForPoly = maskedImageView.frame;
// create our path inside the rect
UIBezierPath *nonagon = [UIBezierPath bezierPathWithPolygonInRect:boundsForPoly numberOfSides:6];
// center the polygon on (0,0)
[nonagon applyTransform:CGAffineTransformMakeTranslation(-boundsForPoly.size.width/2, -boundsForPoly.size.height/2)];
// rotate it 90 degrees
[nonagon applyTransform:CGAffineTransformMakeRotation(M_PI/2)];
// now move it back so that the top left of its bounding box is (0,0)
[nonagon applyTransform:CGAffineTransformMakeTranslation(nonagon.bounds.size.width/2, nonagon.bounds.size.height/2)];
this will rotate the polygon 90 degrees, and keep it's top left corner at (0,0)
The blue outline is your path before, and the green outline is after the rotation:
There is a problem with the answer from adam.wulf is with the line
[nonagon applyTransform:CGAffineTransformMakeTranslation(nonagon.bounds.size.width/2, nonagon.bounds.size.height/2)];
It doesn't center the polygon to the center of the frame. It should be instead
//Centered version
[nonagon applyTransform:CGAffineTransformMakeTranslation(boundsForPoly.size.width/2, boundsForPoly.size.height/2/2)];
Thus the code should look like following from adam.wulf
// get the size of the image, we'll need this for our path and for later too
CGRect boundsForPoly = maskedImageView.frame;
// create our path inside the rect
UIBezierPath *nonagon = [UIBezierPath bezierPathWithPolygonInRect:boundsForPoly numberOfSides:6];
// center the polygon on (0,0)
[nonagon applyTransform:CGAffineTransformMakeTranslation(-boundsForPoly.size.width/2, -boundsForPoly.size.height/2)];
// rotate it 90 degrees
[nonagon applyTransform:CGAffineTransformMakeRotation(M_PI/2)];
// now move it back so that the top left of its bounding box is (0,0)
[nonagon applyTransform:CGAffineTransformMakeTranslation(boundsForPoly.size.width/2, boundsForPoly.size.height/2/2)];

How can I mirror a UIBezierPath?

I have a UIBezierPath and I would like to get its mirror image. How can I accomplish this?
// Method for generating a path
UIBezierPath *myPath = [self generateAPathInBounds:boundingRect];
// ? now I would like to mirror myPath ?
// Method for generating a path
UIBezierPath *myPath = [self generateAPathInBounds:boundingRect];
// Create two transforms, one to mirror across the x axis, and one to
// to translate the resulting path back into the desired boundingRect
CGAffineTransform mirrorOverXOrigin = CGAffineTransformMakeScale(-1.0f, 1.0f);
CGAffineTransform translate = CGAffineTransformMakeTranslation(boundingRect.width, 0);
// Apply these transforms to the path
[myPath applyTransform:mirrorOverXOrigin];
[myPath applyTransform:translate];
It should be noted that you might need additional code depending on where in the overall view the path you are trying to mirror is.
If you are trying to mirror a path that takes up the whole view (or is at least flush with the 0 coordinate of the axis you are mirroring over), then #wbarksdale's answer will work for you. However, if you are trying to mirror a path thats a small section of the overall view thats somewhere in the middle of the view, then you need to do more work. In general, the algorithm is like this
//this rect should be the bounds of your path within its superviews
//coordinate system
let rect = myPath.bounds
//first, you need to move the view all the way to the left
//because otherwise, if you mirror it in its current position,
//the view will be thrown way off screen to the left
path.apply(CGAffineTransform(translationX: -rect.origin.x, y: 0))
//then you mirror it
path.apply(CGAffineTransform(scaleX: -1, y: 1))
//then, after its mirrored, move it back to its original position
path.apply(CGAffineTransform(translationX: rect.origin.x + rect.width, y: 0))
In general, the algorithm should be
Move the path either to the very left or the very top of the view
depending on whether you are flipping horizontally or vertically, using CGAffineTransform(translateX....)
Mirror the view using CGAffineTransform(scaleX....
Move the view back to its original position using CGAffineTransform(translateX....)
I case your path in not centered to (0,0) this code is correct:
// Method for generating a path
UIBezierPath *myPath = [self generateAPathInBounds:boundingRect];
// Create two transforms, one to mirror across the x axis, and one to
// to translate the resulting path back into the desired boundingRect
CGAffineTransform mirrorOverXOrigin = CGAffineTransformMakeScale(-1.0f, 1.0f);
CGAffineTransform translate = CGAffineTransformMakeTranslation(CGRectGetMidX(boundingRect), 0);
CGAffineTransform translateBack = CGAffineTransformMakeTranslation(-CGRectGetMidX(boundingRect), 0);
// Apply these transforms to the path
[myPath applyTransform:translate];
[myPath applyTransform:mirrorOverXOrigin];
[myPath applyTransform:translateBack];

How to Rotate CAShapeLayer containing UIBezierPath? [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Rotate CGPath without changing its position
I searched and tested a variety of code for a couple of hours and I can't get this to work.
I am adding an arbitrary UIBezierPath at a random location to a CAShapeLayer which gets added to a view. I need to rotate the path so that I can handle device rotations. I can rotate the layer instead of the path. I just need the result to be rotated.
I already have methods to handle transforming the bezier path by scaling and translation. It works great, but now I need to simply rotate 90 degrees left or right.
Any recommendations on how to do this?
Basic code:
UIBezierPath *path = <create arbitrary path>
CAShapeLayer *layer = [CAShapeLayer layer];
[self addPathToLayer:layer
fromPath:path];
// I could get the center of the box but where is the box center for the view it is in?
// CGRect box = CGPathGetPathBoundingBox(path.CGPath);
// layer.anchorPoint = ? How to find the center of the box for the anchor point?
// Rotating here appears to rotate around 0,0 of the view
layer.transform = CATransform3DMakeRotation(DegreesToRadians(-90), 0.0, 0.0, 1.0);
I see the following post:
BezierPath Rotation in a UIView
I suppose I could rotate as-is and then translate the path back into place. I just need to figure out what the translation values would be.
I should also state that what I am seeing after I try to rotate is that the image moves off-screen somewhere. I tried rotating 25 degrees to see movement and it pivots around the view's origin of 0,0 so that if I rotate 90 degrees the image is off-screen. I am running these test WITHOUT rotating the device - just to see how rotation works.
UPDATE #1 - 12/4/2012: For some bizarre reason if I set the position to a value I found empirically it moves the rotated bezier path into the correct position after rotation:
layer.position = CGPointMake(280, 60);
This values are a guess from starting/stopping the app and making adjustments. I have no idea why I need to adjust the position on rotation. The anchor point should be in the center of the layer. However, I did find that both the frame and position of a CAShapeLayer are all ZERO even though the path is set, and also the fact that the path is in the correct position within the view. The 280, 60 position shifts the path into what would be the center of the path bounding box when a rotation of +90 is made. If I change the rotation value I need to adjust the position. I should not have to do this manually adjustment.
I think a last resort is to somehow convert the bezier path to an image and then add it. I found that if I set the layer content to an image, then rotate, it rotates about its center point with no positional adjustment needed. Not so with setting the path.
UPDATE #2 12/4/2012 - I tried setting the frame and with fiddling I get it to center as follows:
CGRect box = CGPathGetPathBoundingBox(path.CGPath);
CGRect rect = CGRectMake(0, 0, box.origin.x + (3.5 * box.size.width), box.origin.y + (3.5 * box.size.height));
layer.frame = rect;
layer.transform = CATransform3DMakeRotation(DegreesToRadians(90), 0.0, 0.0, 1.0);
Why multiply by 3.5? I have no clue. I found that adding the box origin with about 3.5 times the size of the box shifts the rotated CAShapeLayer path to about where it should be.
There must be a better way to do this. This is a better solution than my previous post since the frame size does not depend on the rotation angle. I just don't know why the frame needs to be set to the value I am setting it to. I THOUGHT it should be
CGRectMake(0, 0, box.origin.x + (box.size.width / 2), box.origin.y + (box.size.height / 2));
However, it shifts the image to the left too much.
Another clue I found is that if I set the frame of [self view].frame (the frame of the entire parent view, which is the screen of the iPhone), then rotate, the rotation point is the center of the screen, an the path/image orbits around this center point. This is why I tried shifting the frame to what the center of the path should be so that it orbits around the box center.
UPDATE #3 12/4/2012 - I tried to render the layer as an image. However, it appears that just setting the path of a layer does not make it an "image" in the layer since it is empty
CGRect box = CGPathGetPathBoundingBox(path.CGPath);
layer.frame = box;
UIImage *image = [ImageHelper imageFromLayer:layer]; // ImageHelper library I created
CAShapeLayer *newLayer = [CAShapeLayer layer];
newLayer.frame = CGRectMake(box.origin.x, box.origin.y, image.size.width, image.size.height);
newLayer.contents = (id) image.CGImage;
It appears that rotating the layer with its path set is no different than simply rotating the bezier path itself. I will go back to rotating the bezier path and see if I can fiddle with the position elements or something. There's got to be a solution to this.
Goal: Rotate a UIBezierPath around its center point within the view it was originally created in.
UPDATE #4 12/4/2012 - I ran a series of tests measuring the values needed for translation in order to place a UIBezierPath in its previous center location.
CGAffineTransform rotate = CGAffineTransformMakeRotation(DegreesToRadians(-15));
[path applyTransform:rotate];
// CGAffineTransform translate = CGAffineTransformMakeTranslation(-110, 70); // -45
CGAffineTransform translate = CGAffineTransformMakeTranslation(-52, -58); // -15
[path applyTransform:translate];
However, the ratios of x/y translations do not correspond so I cannot extrapolate what translation is required based on the angle. It appears that 'CGAffineTransformMakeRotation' uses some arbitrary anchor put to make the rotation, which at the moment appears to be maybe (viewWidth / 2, 0). I am making this much harder than it needs to be. There's something I am missing to make a simple rotation so that the center point is maintained. I just need to "spin" the path 90 degrees left or right.
UPDATE #5 12/4/2012 - After running additional tests it appears that the anchor point for rotating a UIBezierPath is the origin from where all of the points were drawn. In this case the origin is 0,0 and all of the points are relative to that point. Therefore, it a rotation is applied, the rotation is occurring around the origin, and is why the path shifts up-right on -90 and up-left on 90. I need to somehow set the anchor point for the rotation to the center so it "spins" around the center, rather than the original origin point. 12 hours spent on this one issue.
After some detailed analysis and graphing the bounding box on paper I found my assertion that the origin of 0,0 is correct.
A solution to this problem is to translate the path (the underlying matrix) to the origin, with the center of the bounding box at origin, rotate, then translate the path back to its original location.
Here's how to rotate a UIBezierPath 90 degrees:
CGAffineTransform translate = CGAffineTransformMakeTranslation(-1 * (box.origin.x + (box.size.width / 2)), -1 * (box.origin.y + (box.size.height / 2)));
[path applyTransform:translate];
CGAffineTransform rotate = CGAffineTransformMakeRotation(DegreesToRadians(90));
[path applyTransform:rotate];
translate = CGAffineTransformMakeTranslation((box.origin.x + (box.size.width / 2)), (box.origin.y + (box.size.height / 2)));
[path applyTransform:translate];
Plug in -90 degrees to rotate in the other direction.
This formula can be used when rotating the device from portrait to landscape and vice/versa.
I still don't think this is the ideal solution but the result is what I need for now.
If anyone has a better solution for this please post.
UPDATE 12/7/2012 - I found what I think is the best solution, and very simple as I though it would be. Rather than using rotate, translate, and scale methods on the bezier path, I instead extract the array of points as CGPoint objects, and scale/translate them as needed based on the view size as well as the orientation. I then create a new bezier path and set the layer to this path.
The result is perfect scaling, translation, rotation.

Resources