CABasicAnimation runs after Implicit Animation - ios

I want a layer to behave like this:
Instead, it behaves like this:
The card flip animation is created by two CABasicAnimations applied in a CAAnimationGroup. The incorrect spin effect happens because the implicit animation from the CALayer property change runs first and then my animation specified in the CABasicAnimation runs. How can I stop the implicit animation from running so that only my specified animation runs?
Here's the relevant code:
class ViewController: UIViewController {
var simpleLayer = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
self.view.addGestureRecognizer(tap)
simpleLayer.frame = CGRect(origin: CGPoint(x: view.bounds.width / 2 - 50, y: view.bounds.height / 2 - 50), size: CGSize(width: 100, height: 100))
simpleLayer.backgroundColor = UIColor.blackColor().CGColor
view.layer.addSublayer(simpleLayer)
}
func handleTap() {
let xRotation = CABasicAnimation(keyPath: "transform.rotation.x")
xRotation.toValue = 0
xRotation.byValue = M_PI
let yRotation = CABasicAnimation(keyPath: "transform.rotation.y")
yRotation.toValue = 0
yRotation.byValue = M_PI
simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.y")
simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.x")
let group = CAAnimationGroup()
group.animations = [xRotation, yRotation]
group.duration = 0.6
group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
simpleLayer.addAnimation(group, forKey: nil)
}
}

#LucasTizma had the correct answer.
Surround your animation with CATransaction.begin(); CATransaction.setDisableActions(true) and CATransaction.commit(). This will disable the implicit animation and make the CAAnimationGroup animate correctly.
Here's the final result:
This is the important snippet of code in Swift 3:
CATransaction.begin()
CATransaction.setDisableActions(true)
let xRotation = CABasicAnimation(keyPath: "transform.rotation.x")
xRotation.toValue = 0
xRotation.byValue = M_PI
let yRotation = CABasicAnimation(keyPath: "transform.rotation.y")
yRotation.toValue = 0
yRotation.byValue = M_PI
simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.x")
simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.y")
let group = CAAnimationGroup()
group.animations = [xRotation, yRotation]
simpleLayer.add(group, forKey: nil)
CATransaction.commit()
And this is the full code for the depicted animation with an iOS app:
class ViewController: UIViewController {
var simpleLayer = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
self.view.addGestureRecognizer(tap)
let ratio: CGFloat = 1 / 5
let viewWidth = view.bounds.width
let viewHeight = view.bounds.height
let layerWidth = viewWidth * ratio
let layerHeight = viewHeight * ratio
let rect = CGRect(origin: CGPoint(x: viewWidth / 2 - layerWidth / 2,
y: viewHeight / 2 - layerHeight / 2),
size: CGSize(width: layerWidth, height: layerHeight))
let topRightPoint = CGPoint(x: rect.width, y: 0)
let bottomRightPoint = CGPoint(x: rect.width, y: rect.height)
let topLeftPoint = CGPoint(x: 0, y: 0)
let linePath = UIBezierPath()
linePath.move(to: topLeftPoint)
linePath.addLine(to: topRightPoint)
linePath.addLine(to: bottomRightPoint)
linePath.addLine(to: topLeftPoint)
let maskLayer = CAShapeLayer()
maskLayer.path = linePath.cgPath
simpleLayer.frame = rect
simpleLayer.backgroundColor = UIColor.black.cgColor
simpleLayer.mask = maskLayer
// Smooth antialiasing
// * Convert the layer to a simple bitmap that's stored in memory
// * Saves CPU cycles during complex animations
// * Rasterization is set to happen during the animation and is disabled afterwards
simpleLayer.rasterizationScale = UIScreen.main.scale
view.layer.addSublayer(simpleLayer)
}
func handleTap() {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock({
self.simpleLayer.shouldRasterize = false
})
simpleLayer.shouldRasterize = true
let xRotation = CABasicAnimation(keyPath: "transform.rotation.x")
xRotation.toValue = 0
xRotation.byValue = M_PI
let yRotation = CABasicAnimation(keyPath: "transform.rotation.y")
yRotation.toValue = 0
yRotation.byValue = M_PI
simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.x")
simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.y")
let group = CAAnimationGroup()
group.animations = [xRotation, yRotation]
group.duration = 1.2
group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
simpleLayer.add(group, forKey: nil)
CATransaction.commit()
}
}

You are creating 2 separate animations and applying them in an animation group. When you do that they are applied as 2 discrete steps.
It looks like that's not what you want. If not, then don't create 2 separate animations, one on transform.rotation.x and the other on transform.rotation.y. Instead, concatenate both changes onto a transformation matrix and apply the changed transformation matrix as a single animation.

Related

animate CAShapeLayer on circular path

I want to animate my thumb layer on the drawn path. I can animate the progress layer as per the value but, moving the entire layer doesn't work.
func setProgress(_ value:Double, _ animated :Bool) {
let progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
progressAnimation.duration = animated ? 0.6 : 0.0
progressAnimation.fromValue = currentValue
progressAnimation.toValue = value
progressAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
progressLayer.strokeEnd = CGFloat(value)
progressLayer.add(progressAnimation, forKey: "animateprogress")
}
Using this function I can animate the progress layer. and I'm stuck on animating thumb position.
I tried some things but, it's not working.
failed attempt 1
guard let path = thumbLayer.path else {return}
let center = CGPoint(x: frame.width/2, y: frame.height/2)
let radius = min(frame.width, frame.height)/2 - max(trackWidth, max(progressWidth, thumbWidth))/2
let thumbAngle = 2 * .pi * currentValue - (.pi/2)
let thumbX = CGFloat(cos(thumbAngle)) * radius
let thumbY = CGFloat(sin(thumbAngle)) * radius
let newThumbCenter = CGPoint(x: center.x + thumbX, y: center.y + thumbY)
let thumbPath = UIBezierPath(arcCenter: newThumbCenter, radius: thumbWidth/2, startAngle: -.pi/2, endAngle: .pi*1.5, clockwise: true)
let thumbAnimation = CABasicAnimation(keyPath: "path")
thumbAnimation.duration = animated ? 0.6 : 0.0
thumbAnimation.fromValue = path
thumbAnimation.toValue = thumbPath.cgPath
thumbAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
thumbLayer.strokeEnd = CGFloat(value)
thumbLayer.add(thumbAnimation, forKey: "animateprogress")
failed attempt 2
let intialPosition = thumbLayer.position
let center = CGPoint(x: frame.width/2, y: frame.height/2)
let radius = min(frame.width, frame.height)/2 - max(trackWidth, max(progressWidth, thumbWidth))/2
let thumbAngle = 2 * .pi * currentValue - (.pi/2)
let thumbX = CGFloat(cos(thumbAngle)) * radius
let thumbY = CGFloat(sin(thumbAngle)) * radius
let newThumbCenter = CGPoint(x: center.x + thumbX - thumbCenter.x, y: center.y + thumbY - thumbCenter.y)
thumbLayer.position.x = newThumbCenter.x
let thumbAnimationX = CABasicAnimation(keyPath: "progress.x")
thumbAnimationX.duration = animated ? 0.6 : 0.0
thumbAnimationX.fromValue = intialPosition.x
thumbAnimationX.toValue = newThumbCenter.x
thumbAnimationX.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
thumbLayer.add(thumbAnimationX, forKey: "progress.x")
thumbLayer.position.y = newThumbCenter.y
let thumbAnimationY = CABasicAnimation(keyPath: "progress.y")
thumbAnimationY.duration = animated ? 0.6 : 0.0
thumbAnimationY.fromValue = intialPosition.y
thumbAnimationY.toValue = newThumbCenter.y
thumbAnimationY.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
thumbLayer.add(thumbAnimationY, forKey: "progress.y")
In your first attempt, you're trying to animate the thumb's path (the outline of the red circle). In your second attempt, you're trying to animate the position of thumbLayer (but you animated the wrong key paths).
Since you're having difficulty making either of those work, let's try a different approach. Take a look at the standard Swiss railway clock:
Specifically, look at the red dot at the end of the second hand. How does the clock move that red dot around the face? By rotating the second hand around the center of the face.
That's what we're going to do, but we won't draw the whole second hand. We'll just draw the red dot at the end.
We'll put the origin of the thumbLayer's coordinate system at the center of the circular track, and we'll set its path so that the thumb appears on the path. Here's the layout:
Here's code to set up a circleLayer and a thumbLayer:
private let circleLayer = CAShapeLayer()
private let thumbLayer = CAShapeLayer()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let circleRadius: CGFloat = 100
let thumbRadius: CGFloat = 22
let circleCenter = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
circleLayer.fillColor = nil
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.lineWidth = 4
circleLayer.lineJoin = .round
circleLayer.path = CGPath(ellipseIn: CGRect(x: -circleRadius, y: -circleRadius, width: 2 * circleRadius, height: 2 * circleRadius), transform: nil)
circleLayer.position = circleCenter
view.layer.addSublayer(circleLayer)
thumbLayer.fillColor = UIColor.red.cgColor
thumbLayer.strokeColor = nil
thumbLayer.path = CGPath(ellipseIn: CGRect(x: -thumbRadius, y: -circleRadius - thumbRadius, width: 2 * thumbRadius, height: 2 * thumbRadius), transform: nil)
thumbLayer.position = circleCenter
view.layer.addSublayer(thumbLayer)
}
With this layout, we can move the thumb along the track without changing its position. Instead, we rotate thumbLayer around its origin:
#IBAction func buttonWasTapped() {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = 2 * CGFloat.pi
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.duration = 1.5
animation.isAdditive = true
thumbLayer.add(animation, forKey: nil)
}
Result:

Custom indicator with rotate blink animation like UIActivityIndicatorView

I am trying to make custom activity indicator, see the indicator class below
import UIKit
class MyIndicator: UIView {
let gap = CGFloat(.pi/4 / 6.0)
var count = 0
override func draw(_ rect: CGRect) {
super.draw(rect)
}
func blink() {
backgroundColor = .clear
let duration: CFTimeInterval = 1.2
//let beginTime = CACurrentMediaTime()
let beginTimes: [CFTimeInterval] = [0.25, 1, 1.75, 2.5]
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// Animation
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.keyTimes = [0, 0.5, 1]
animation.timingFunctions = [timingFunction, timingFunction]
animation.values = [1, 0.3, 1]
animation.duration = duration
animation.repeatCount = HUGE
animation.isRemovedOnCompletion = false
for i in 0...3 {
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.fillColor = UIColor.clear.cgColor
shape.lineWidth = 6.8
shape.strokeColor = UIColor.blue.cgColor
let startAngle:CGFloat = CGFloat(i) * CGFloat(Double.pi/2) + gap
let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap * 2
shape.path = UIBezierPath(arcCenter: center, radius: -20, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
animation.beginTime = beginTimes[i]
shape.add(animation, forKey: "animation")
self.layer.addSublayer(shape)
}
}
func startAnimating() {
blink()
}
}
let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
self.view.addSubview(indicator)
indicator.startAnimating()
I have attached my current result.
But you can see that the animation is not in circular motion like standard UIActivityIndicatorView. Can anyone help me to fix this.
Try using a CAReplicatorLayer and instance delay to get everything in sync. Here is a Playground. I am not 100% sure on the visual you want but this should be close.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyIndicator: UIView {
let gap = CGFloat(.pi/4 / 6.0)
private var replicatorLayer = CAReplicatorLayer()
private var mainShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
func commonSetup(){
mainShapeLayer = CAShapeLayer()
mainShapeLayer.frame = self.bounds
mainShapeLayer.fillColor = UIColor.clear.cgColor
mainShapeLayer.lineWidth = 6.8
mainShapeLayer.strokeColor = UIColor.blue.cgColor
let startAngle:CGFloat = CGFloat(Double.pi * 2) + gap/2
let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap/2
mainShapeLayer.path = UIBezierPath(arcCenter: center, radius: self.bounds.midX - 10, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = self.bounds
replicatorLayer.instanceCount = 4
let angle = (Double.pi * 2)/4
replicatorLayer.instanceTransform = CATransform3DRotate(CATransform3DIdentity, CGFloat(angle), 0, 0, 1)
replicatorLayer.addSublayer(mainShapeLayer)
replicatorLayer.opacity = 0
self.layer.addSublayer(replicatorLayer)
}
func animate(){
let defaultDuration : Double = 0.75
let animate = CAKeyframeAnimation(keyPath: "opacity")
animate.values = [1, 0.3, 1]
animate.keyTimes = [0, 0.5, 1]
animate.repeatCount = .greatestFiniteMagnitude
animate.duration = defaultDuration
animate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
replicatorLayer.instanceDelay = defaultDuration/4
self.mainShapeLayer.add(animate, forKey: nil)
let opacityIn = CABasicAnimation(keyPath: "opacity")
opacityIn.fromValue = 1
opacityIn.toValue = 0
opacityIn.duration = 0.2
replicatorLayer.add(opacityIn, forKey: nil)
self.replicatorLayer.opacity = 1
}
func stopAnimating(){
CATransaction.begin()
let opacityOut = CABasicAnimation(keyPath: "opacity")
opacityOut.fromValue = 1
opacityOut.toValue = 0
opacityOut.duration = 0.2
CATransaction.setCompletionBlock {
[weak self] in
self?.mainShapeLayer.removeAllAnimations()
}
replicatorLayer.add(opacityOut, forKey: nil)
self.replicatorLayer.opacity = 0
CATransaction.commit()
}
override func layoutSubviews() {
super.layoutSubviews()
mainShapeLayer.frame = self.bounds
replicatorLayer.frame = self.bounds
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
indicator.animate()
//just to simulate starting and stoping
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 10) {
indicator.stopAnimating()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
indicator.animate()
}
}
view.addSubview(indicator)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Update uilabel percentage with animation of progress circle swift

I have a circle that is animating itself. It is given a number between 0.01 and 1.0 and it animates after that in a circle.
I have a label in the middle of it called progressLabel. I would like the label to count along as the circle animates. So if the value is at 0.12, the label should display 12%.
Here is the code for my circle animating:
func animateView(toValue: Double, strokeColor: UIColor) {
let screenWidth = self.view.frame.size.width
let screenHeight = self.view.frame.size.height
let circle = UIView(frame: CGRectMake((screenWidth / 2) - (150 / 2), (screenHeight / 2) - (150 / 2), 150, 150)) // viewProgress is a UIView
circle.backgroundColor = UIColor.clearColor()
view.addSubview(circle)
var progressCircle = CAShapeLayer()
var backgroundCircle = CAShapeLayer()
progressCircle.frame = view.bounds
backgroundCircle.frame = view.bounds
let lineWidth:CGFloat = 20
let rectFofOval = CGRectMake(lineWidth / 2, lineWidth / 2, circle.bounds.width - lineWidth, circle.bounds.height - lineWidth)
let circlePath = UIBezierPath(ovalInRect: rectFofOval)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.whiteColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 20.0
progressCircle.frame = view.bounds
progressCircle.lineCap = "round"
backgroundCircle = CAShapeLayer ()
backgroundCircle.path = circlePath.CGPath
backgroundCircle.strokeColor = strokeColor.CGColor
backgroundCircle.fillColor = UIColor.clearColor().CGColor
backgroundCircle.lineWidth = 20.0
backgroundCircle.frame = view.bounds
backgroundCircle.lineCap = "round"
circle.layer.addSublayer(backgroundCircle)
circle.layer.addSublayer(progressCircle)
circle.transform = CGAffineTransformRotate(circle.transform, CGFloat(-M_PI_2))
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = toValue
animation.duration = 1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
progressCircle.addAnimation(animation, forKey: nil)
}
I have tried this but the label just keeps displaying 0.0%..
var current: Double = 0.0
let i = current * 100
let max = 0.1 * 100
if i < max {
self.progressLabel.text = "\(current)%"
current += 0.01 * 100
}
You should check the values of the presentation layer to get the current value of the animated property.
func printValue() {
let currentLayer = progressCircle.presentation()
current = currentLayer?.value(forKeyPath: "strokeEnd") as? Float;
print("current \(current)")
let i = current! * 100
let max:Float = 100
if i < max {
self.progressLabel.text = "\(current! * 100)%"
}
}
Call the method printValue from where ever you want to display the current value

Animating circular progress View in Swift

I want to have an animation for my circular progressView with the code below. I want the animation start the progress from 0 to the value I want.
But I can never get the animation work.
func Popular(){
let circle = Pop;//Pop is a UIView
circle.bounds = CGRect(x: 0,y: 0, width: 100, height: 100);
circle.frame = CGRectMake(0,0, 100, 100);
circle.layoutIfNeeded()
var progressCircle = CAShapeLayer();
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2);
let circleRadius : CGFloat = circle.bounds.width / 2 * 0.83;
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.5 * M_PI), endAngle: CGFloat(1.5 * M_PI), clockwise: true );
progressCircle = CAShapeLayer ();
progressCircle.path = circlePath.CGPath;
progressCircle.strokeColor = UIColor.greenColor().CGColor;
progressCircle.fillColor = UIColor.clearColor().CGColor;
UIView.animateWithDuration(3.0,
delay: 1.0,
options: [],
animations: {
progressCircle.lineWidth = 5.0;
progressCircle.strokeStart = 0;
progressCircle.strokeEnd = 0.20;
circle.layer.addSublayer(progressCircle);
circle
progressCircle.strokeEnd = 0.2;
},
completion: { finished in
print("Picutre done")
})
}
Use CABasicAnimation
let circle = Pop
var progressCircle = CAShapeLayer()
let circlePath = UIBezierPath(ovalInRect: circle.bounds)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.greenColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 5.0
circle.layer.addSublayer(progressCircle)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 0.2
animation.duration = 3
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
progressCircle.addAnimation(animation, forKey: "ani")
Two things -
First - Do not re-initialize progressCircle. You are calling progressCircle = CAShapeLayer (); twice. This is not bug here though.
Second - Add your sublayer before you start animating it. Move circle.layer.addSublayer(progressCircle); outside the animation block.

Substract CALayer from another, blend?

On the following image, the blue circle is only for debug purpose. My goal is that every layer behind the blue circle should be transparent.I only want to keep visible what's outside the blue circle.
Here is the code written in swift :
let croissantView = UIView(frame: CGRectMake(100, 100, 100, 100))
croissantView.backgroundColor = UIColor.clearColor()
self.view.addSubview(croissantView)
let radius = 50 as CGFloat
let width = 50 as CGFloat
let origin = CGPointMake(0, 100)
let end = CGPointMake(100,100)
let highAmplitude = CGPointMake(50,180)
let lowAmplitude = CGPointMake(50,150)
let quad = UIBezierPath()
quad.moveToPoint(origin)
quad.addQuadCurveToPoint(end, controlPoint: highAmplitude)
quad.closePath()
let quad2 = UIBezierPath()
quad2.moveToPoint(origin)
quad2.addQuadCurveToPoint(end, controlPoint: lowAmplitude)
quad2.closePath()
let layer = CAShapeLayer()
layer.path = quad.CGPath
layer.strokeColor = UIColor.greenColor().CGColor
layer.fillColor = UIColor.blackColor().CGColor
croissantView.layer.addSublayer(layer)
let anim = CABasicAnimation(keyPath: "path")
anim.duration = 3
anim.repeatCount = 20
anim.fromValue = quad.CGPath
anim.toValue = quad2.CGPath
anim.autoreverses = true
layer.addAnimation(anim, forKey: "animQuad")
let animRotate = CABasicAnimation(keyPath: "transform.rotation")
animRotate.duration = 5
animRotate.repeatCount = 20
animRotate.fromValue = 0
animRotate.toValue = 360 * CGFloat(M_PI) / 180
croissantView.layer.addAnimation(animRotate, forKey: "animRotate")
let circle = UIBezierPath(arcCenter: croissantView.center, radius: 75, startAngle: 0, endAngle: 360 * CGFloat(M_PI) / 180, clockwise: true);
let circleLayer = CAShapeLayer()
circleLayer.path = circle.CGPath
circleLayer.fillColor = UIColor.blueColor().CGColor
circleLayer.opacity = 0.6
self.view.layer.addSublayer(circleLayer)
Thoughts
Substract from Layer, don't know if feasible
Blend, don't know how to do it with CALayer
Thanks !! :)
There are three things I would suggest.
Change your croissant view to a layer (not sure this was necessary, but this is what I did)
Create a CGPath for your circle shape that can have its mask inverted. You do this by wrapping an outer rect around your circle so that the path looks like this:
Set your fill rule on your circle shape layer to even-odd:
circleLayer.fillRule = kCAFillRuleEvenOdd
This is what it looks like:
I adapted your code to attempt it and this is what I came up with. Your mileage may vary:
func applyMask() {
let mainLayer = CALayer()
mainLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
mainLayer.position = CGPoint(x: 200.0, y: 200.0)
// Set the color for debugging
mainLayer.backgroundColor = UIColor.orangeColor().CGColor
self.view.layer.addSublayer(mainLayer)
let radius = 50 as CGFloat
let width = 50 as CGFloat
let origin = CGPointMake(0, 100)
let end = CGPointMake(100,100)
let highAmplitude = CGPointMake(50,180)
let lowAmplitude = CGPointMake(50,150)
let quad = UIBezierPath()
quad.moveToPoint(origin)
quad.addQuadCurveToPoint(end, controlPoint: highAmplitude)
quad.closePath()
let quad2 = UIBezierPath()
quad2.moveToPoint(origin)
quad2.addQuadCurveToPoint(end, controlPoint: lowAmplitude)
quad2.closePath()
let layer = CAShapeLayer()
layer.path = quad.CGPath
layer.strokeColor = UIColor.greenColor().CGColor
layer.fillColor = UIColor.blackColor().CGColor
mainLayer.addSublayer(layer)
let anim = CABasicAnimation(keyPath: "path")
anim.duration = 3
anim.repeatCount = 20
anim.fromValue = quad.CGPath
anim.toValue = quad2.CGPath
anim.autoreverses = true
layer.addAnimation(anim, forKey: "animQuad")
let animRotate = CABasicAnimation(keyPath: "transform.rotation")
animRotate.duration = 5
animRotate.repeatCount = 20
animRotate.fromValue = 0
animRotate.toValue = 360 * CGFloat(M_PI) / 180
mainLayer.addAnimation(animRotate, forKey: "animRotate")
// Build a Circle CGPath
var circlePath = CGPathCreateMutable()
CGPathAddEllipseInRect(circlePath, nil, CGRectInset(mainLayer.bounds, -20.0, -20.0))
// Invert the mask by adding a bounding rectangle
CGPathAddRect(circlePath, nil, CGRectInset(mainLayer.bounds, -100.0, -100.0))
CGPathCloseSubpath(circlePath)
let circleLayer = CAShapeLayer()
circleLayer.fillColor = UIColor.blueColor().CGColor
circleLayer.opacity = 0.6
// Use the even odd fill rule so that our path gets inverted
circleLayer.fillRule = kCAFillRuleEvenOdd
circleLayer.path = circlePath
mainLayer.mask = circleLayer
mainLayer.addSublayer(layer)
}
Here's the project where I tried it out: https://github.com/perlmunger/SwiftVertedMask
Hope that helps.

Resources