CAShapeLayer path animation shrinking before completing animation - ios

I am trying to animate a rotation of a layer's CGpath.
The rotation works perfectly, but for some reason, the path seems to shrink, then get bigger again. I just want it to show a rotating animation. I don't want it to shrink
here is a video.
here is the code
class ViewController: UIViewController {
#IBOutlet weak var overlayView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let maskLayer = CAShapeLayer()
let path = UIBezierPath(rect: view.bounds)
let mask = UIBezierPath.drawSquare(width: 100, center: view.center)
// Makes it so we can see through the center of the view
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
path.append(mask)
maskLayer.path = path.cgPath
overlayView.layer.mask = maskLayer
}
#IBAction func rotateView(_ sender: Any) {
let maskPath = UIBezierPath.drawSquare(width: 100, center: overlayView.center)
let path = UIBezierPath(rect: overlayView.frame)
let bounds: CGRect = maskPath.cgPath.boundingBox
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radians = 90 / 180.0 * .pi
var transform: CGAffineTransform = .identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y)
maskPath.apply(transform)
path.append(maskPath)
// create new animation
let anim = CABasicAnimation(keyPath: "path")
anim.toValue = path.cgPath
(overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "path")
}
}
extension UIBezierPath{
static func drawSquare(width: CGFloat, center: CGPoint) -> UIBezierPath {
let rect = CGRect(x: center.x - width/2, y: center.y - width/2, width: width, height: width)
let path = UIBezierPath()
path.move(to: rect.origin)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.close()
return path
}
}

Your "cutout box" is shrinking and growing because you're using path animation.
When you animate a path from one set of points to another, each point will take a straight line from its starting point to its ending point.
Couple ways to visualize this...
First, we'll add an "outline" frame to the cutout mask box:
as you see, each corner is taking a direct route to its next position.
If we highlight one side, it's maybe a bit more obvious:
and, we can number the corners for even more clarity:
So, if we want to rotate the square without changing its size, we could use keyframe animation and run the corners along a circle:
or, much easier, rotate the mask layer instead of its path:
let radians = 90 / 180.0 * .pi
let anim = CABasicAnimation(keyPath: "transform.rotation.z")
anim.toValue = radians
anim.duration = 1.0
(overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "layerRotate")
No doubt we notice that the mask layer frame does not completely cover the overlayView frame.
So, we can fix that by making the mask layer frame larger.
We could calculate exactly how big it needs to be, but because shape layers are very efficient, we'll just make it a square, 1.5 times bigger than the longer axis:
so when it rotates, it covers the view:
and we finish with this:

Related

How to draw a UIView or a UIButton using UIBezierPath

I want to draw a UIView or a UIButton like this one which is show in the image.
How it is possible to draw in swift?
I have done a lot of R $ D on this what I get is UIBezierPath.
So, How to draw using UIBezierPath. I am unable to draw exactly this.
Please help me to draw like this.
I want create a separate class for this so that I can inherit any where.
Thank you in advance.
I have tried something like this.
extension UIButton {
func roundCorners(corners: UIRectCorner, radius: Int = 8) {
let maskPath1 = UIBezierPath(roundedRect: bounds,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius))
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = bounds
maskLayer1.path = maskPath1.cgPath
layer.mask = maskLayer1
}
}
Result is
But I want a sharp cut.Like first image
To draw that shape, you want to use maskPath1.move(to: pt) and then a series of maskPath1.addLine(to: pt).
You'll need six points:
so you could code it like this:
extension UIButton {
func angledCorners() {
let maskLayer1 = CAShapeLayer()
maskLayer1.frame = bounds
let pt1: CGPoint = CGPoint(x: bounds.minX, y: bounds.maxY)
let pt2: CGPoint = CGPoint(x: bounds.minX, y: bounds.midY)
let pt3: CGPoint = CGPoint(x: bounds.minX + bounds.midY, y: bounds.minY)
let pt4: CGPoint = CGPoint(x: bounds.maxX, y: bounds.minY)
let pt5: CGPoint = CGPoint(x: bounds.maxX, y: bounds.midY)
let pt6: CGPoint = CGPoint(x: bounds.maxX - bounds.midY, y: bounds.maxY)
let maskPath1: UIBezierPath = UIBezierPath()
maskPath1.move(to: pt1)
maskPath1.addLine(to: pt2)
maskPath1.addLine(to: pt3)
maskPath1.addLine(to: pt4)
maskPath1.addLine(to: pt5)
maskPath1.addLine(to: pt6)
maskPath1.close()
maskLayer1.path = maskPath1.cgPath
layer.mask = maskLayer1
}
}
A couple problems with that approach though... the mask will not "auto-resize" when the button frame changes. So, you have to wait to call button.angledCorners() until auto-layout has finished setting the button's frame, and you have to call it again any time the frame changes.
If you have explicit frame sizes, that's not a problem.
But if you have buttons that change based on other views, or perhaps change size when the device is rotated, it's a hassle... and you probably want to make this a feature of a custom subclass.

How to add a custom shape to an UIImageView in Swift?

I'm trying to add a custom shape to an imageView. Please check the below images.
This is the required one:
This is what I have done so far:
I'm new to Core Graphics and I have done this so far:
private func customImageClipper(imageV: UIImageView){
let path = UIBezierPath()
let size = imageV.frame.size
print(size)
path.move(to: CGPoint(x: 0.0, y: size.height))
path.addLine(to: CGPoint(x: 0.8, y: size.height/2))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
imageV.layer.sublayers = [shape]
}
I'm creating a function to achieve a shape like this, but whenever I pass the imageView into this function, I can not see any change at all. I know that I have to move from points to another point to achieve this shape, but I have never done this. Any help would be appreciated. This is how I'm calling this function:
imageV.layoutIfNeeded()
customImageClipper(imageV: imageV)
P.S.: I'm not using Storyboard, I have created this programmatically.
There are many ways to create shapes using UIBezierPaths. This post here discusses the use of the draw function to create a shape.
Here is an example using your clip function within the cell.
func clip(imageView: UIView, withOffset offset: CGFloat) {
let path = UIBezierPath()
//Move to Top Left
path.move(to: .init(x: imageView.bounds.size.width * offset, y: 0))
//Draw line from Top Left to Top Right
path.addLine(to: .init(x: imageView.bounds.size.width, y: 0))
//Draw Line from Top Right to Bottom Right
path.addLine(to: .init(x: imageView.bounds.size.width * (1 - offset), y: imageView.bounds.size.height))
//Draw Line from Bottom Right to Bottom Left
path.addLine(to: .init(x: 0, y: imageView.bounds.size.height))
//Close Path
path.close()
//Create the Shape Mask for the ImageView
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.black.cgColor
imageView.layer.mask = shapeLayer
}
In this function, the offset is the amount of angle you would like on the shape, ranging from 0 to 1. (0.4) seems to work for your requirements.
This shares a lot of similarities with Apseri's answer, except I chose the route of percentages, rather than exact size. Nothing wrong with either approach, I just found it easier to understand with percentages. :)
One last note to point out, I used this function in the layoutSubviews function.
override func layoutSubviews() {
super.layoutSubviews()
imageView.layoutIfNeeded()
clip(imageView: self.imageView, withOffset: 0.4)
}
This output the following image:
Hope this helps.
Here is example of some path clipping. Of course path can be also put via parameters, and this can be applied to any view, as shown.
Before:
After (grey background is below ScrollView background):
func customImageClipper(imageV: UIView){
let path = UIBezierPath()
let size = imageV.frame.size
path.move(to: CGPoint(x: size.width/3.0, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0 + 50, y: 0))
path.addLine(to: CGPoint(x: size.width/3.0, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0 - 50, y: size.height))
path.addLine(to: CGPoint(x: size.width/3.0, y: 0))
path.close()
let shape = CAShapeLayer()
shape.path = path.cgPath
shape.fillColor = UIColor.black.cgColor
imageV.layer.mask = shape
}
1- Subclassing your UIImageView
2- implement your custom drawings inside setNeedsLayout using UIBezierPath
class MyCustomImageView: UIImageView {
override func setNeedsLayout() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.addLineToPoint(CGPoint(x: self.frame.size.width, y: self.frame.size.height/2))
path.addLineToPoint(CGPoint(x: self.frame.size.width/2, y: 0))
path.addArcWithCenter(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: self.frame.size.width/2, startAngle:-CGFloat(M_PI_2), endAngle: CGFloat(M_PI_2), clockwise: false)
path.moveToPoint(CGPoint(x: self.frame.size.width/2, y: self.frame.size.height))
path.closePath()
UIColor.redColor().setFill()
path.stroke()
path.bezierPathByReversingPath()
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.path = path.CGPath
shapeLayer.fillColor = UIColor.redColor().CGColor
self.layer.mask = shapeLayer;
self.layer.masksToBounds = true;
}
}

UIView pauses during animation along UIBezierPath

I'm trying to animate a UIView(a square) to move along a UIBezierPath using a CAKeyframeAnimation. The square pauses at two points along the bezier path, both points being right before the path begins to arc.This is my code for the UIBezierPath and Animation:
func createTrack() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: layerView.frame.size.width/2, y: 0.0))
path.addLine(to: CGPoint(x: layerView.frame.size.width - 100.0, y: 0.0))
path.addArc(withCenter: CGPoint(x: layerView.frame.size.width - 100.0,y: layerView.frame.height/2), radius: layerView.frame.size.height/2, startAngle: CGFloat(270).toRadians(), endAngle: CGFloat(90).toRadians(), clockwise: true)
path.addLine(to: CGPoint(x: 100.0, y: layerView.frame.size.height))
path.addArc(withCenter: CGPoint(x: 100.0,y: layerView.frame.height/2), radius: layerView.frame.size.height/2, startAngle: CGFloat(90).toRadians(), endAngle: CGFloat(270).toRadians(), clockwise: true)
path.close()
return path
}
#IBAction func onAnimatePath(_ sender: Any) {
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 20, height: 20)
square.backgroundColor = UIColor.red
layerView.addSubview(square)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = trackPath.cgPath
animation.rotationMode = kCAAnimationRotateAuto
animation.repeatCount = Float.infinity
animation.duration = 60
square.layer.add(animation, forKey: "animate position along path")
}
layerView is just a UIView. Any ideas on why this happens and how I can fix this?
The path you are using consists of 5 segments (two arcs and three lines (including the one when you close the path)) and the animation spends the same time on each of the segments. If this path if too narrow, these lines segments will have no length and the square will appear still for 12 seconds in each of them.
You probably want to use a "paced" calculation mode to achieve a constant velocity
animation.calculationMode = kCAAnimationPaced
This way, the red square will move at a constant pace – no matter how long each segment of the shape is.
This alone will give you the result you are after, but there's more you can do to simplify the path.
The addArc(...) method is rather smart and will add a line from the current point to the start of the arc itself, so the two explicit lines aren't needed.
Additionally, if you change the initial point you're moving the path to to have the same x component as the center of the second arc, then the path will close itself. Here, all you need are the two arcs.
That said, if the shape you're looking to create is a rounded rectangle like this, then you can use the UIBezierPath(roundedRect:cornerRadius:) convenience initializer:
let radius = min(layerView.bounds.width, layerView.bounds.height) / 2.0
let path = UIBezierPath(roundedRect: layerView.bounds, cornerRadius: radius)

Masking a CAShapeLayer creates this weird artifact?

I want to create a smooth progress bar for a to-do list. I used a circle (CGMutablePath) that masks the gray area and there's an obvious arc-like artifact. Not only that but there's also an artifact on the right side of the bar.
Note: I tried to rasterize the layers to no avail.
What causes iOS to do this and how can I smooth this out or get rid of it?
private var circleMask: CAShapeLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = GradientColor(.leftToRight, frame: self.bounds, colors: GradientFlatRed())
layer.cornerRadius = bounds.height / 2
plainProgressBar.layer.cornerRadius = layer.cornerRadius
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
plainProgressBar.layer.shouldRasterize = true
plainProgressBar.layer.rasterizationScale = UIScreen.main.scale
createMask()
}
private func createMask() {
let path = CGMutablePath()
path.addArc(center: CGPoint(x: bounds.origin.x+25/2, y: bounds.origin.y+25/2), radius: 25/2, startAngle: 0, endAngle: CGFloat(Double.pi*2), clockwise: false)
path.addRect(bounds)
circleMask.path = path
circleMask.backgroundColor = plainMeterColor.cgColor
circleMask.fillRule = kCAFillRuleEvenOdd
plainProgressBar.layer.mask = circleMask
}
The issue is clearly an error in composition of the antialiased edges.
Knee-jerk question: do you have an atypical blend mode set?
Easiest solution: don't use path.addArc and path.addRect to generate two completely distinct shapes. Just use one of the init(roundedRect:... methods and set a corner radius of half your height, stretching the total path beyond the available space to the left to create a hard edge.
Or, if that doesn't appear, construct the path manually as a move, addLine(to:, addArc of only half a circle, then a final addLine(to: and a close. E.g. (thoroughly untested, especially re: start and end angles and clockwise versus anticlockwise versus iOS's upside-down coordinate system)
private func createMask() {
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y:0))
path.addLine(to: CGPoint(x: bounds.origin.x+25/2, y: 0))
path.addArc(center: CGPoint(x: bounds.origin.x+25/2, y: bounds.origin.y+25/2), radius: 25/2, startAngle: CGFloat(Double.pi/2.0), endAngle: CGFloat(Double.pi*3.0/2.0), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: 25))
path.close()
...

CATransform3DMakeRotation anchorpoint and position in Swift

Hi I am trying to rotate a triangle shaped CAShapeLayer. The northPole CAShapeLayer is defined in a custom UIView class. I have two functions in the class one to setup the layer properties and one to animate the rotation. The rotation animate works fine but after the rotation completes it reverts back to the original position. I want the layer to stay at the angle given (positionTo = 90.0) after the rotation animation.
I am trying to set the position after the transformation to retain the correct position after the animation. I have also tried to get the position from the presentation() method but this has been unhelpful. I have read many articles on frame, bounds, anchorpoints but I still cannot seem to make sense of why this transformation rotation is not working.
private func setupNorthPole(shapeLayer: CAShapeLayer) {
shapeLayer.frame = CGRect(x: self.bounds.width / 2 - triangleWidth / 2, y: self.bounds.height / 2 - triangleHeight, width: triangleWidth, height: triangleHeight)//self.bounds
let polePath = UIBezierPath()
polePath.move(to: CGPoint(x: 0, y: triangleHeight))
polePath.addLine(to: CGPoint(x: triangleWidth / 2, y: 0))
polePath.addLine(to: CGPoint(x: triangleWidth, y: triangleHeight))
shapeLayer.path = polePath.cgPath
shapeLayer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
The animate function:
private func animate() {
let positionTo = CGFloat(DegreesToRadians(value: degrees))
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0.0
rotate.toValue = positionTo //CGFloat(M_PI * 2.0)
rotate.duration = 0.5
northPole.add(rotate, forKey: nil)
let present = northPole.presentation()!
print("presentation position x " + "\(present.position.x)")
print("presentation position y " + "\(present.position.y)")
CATransaction.begin()
northPole.transform = CATransform3DMakeRotation(positionTo, 0.0, 0.0, 1.0)
northPole.position = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)
CATransaction.commit()
}
To fix the problem, in the custom UIView class I had to override layoutSubviews() with the following:
super.layoutSubviews()
let northPoleTransform: CATransform3D = northPole.transform
northPole.transform = CATransform3DIdentity
setupNorthPole(northPole)
northPole.transform = northPoleTransform

Resources