CALayer path animation not working - ios

Why does this path animation not work?
let frame = CGRect(x: 20, y: 20, width: 10, height: 10)
let frame2 = CGRect(x: 20, y: 20, width: 100, height: 100)
let path = UIBezierPath(rect: frame)
let path2 = UIBezierPath(rect: frame2)
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = path
animation.toValue = path2
animation.duration = 3
animation.repeatCount = Float.greatestFiniteMagnitude
let l = CAShapeLayer()
l.path = path.cgPath
l.fillColor = UIColor.black.cgColor
self.view.layer.addSublayer(l)
l.add(animation, forKey: "pathAnimation")
Complete playground:
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
let frame = CGRect(x: 20, y: 20, width: 10, height: 10)
let frame2 = CGRect(x: 20, y: 20, width: 100, height: 100)
let path = UIBezierPath(rect: frame)
let path2 = UIBezierPath(rect: frame2)
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = path
animation.toValue = path2
animation.duration = 3
animation.repeatCount = Float.greatestFiniteMagnitude
let l = CAShapeLayer()
l.path = path.cgPath
l.fillColor = UIColor.black.cgColor
self.view.layer.addSublayer(l)
l.add(animation, forKey: "pathAnimation")
}
}
PlaygroundPage.current.liveView = MyViewController()

You should use cgPath in animation
animation.fromValue = path.cgPath
animation.toValue = path2.cgPath

Related

Mask over the image view Swift 5

I have been trying to implement this answer from this question using Swift 5.2
I only see the blurred image and the mask is not appearing. Can someone point me what I am missing there or how to convert it to Swift 5 from Swift 3?
This is a playground code.
import UIKit
import PlaygroundSupport
let generalFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let containerView = UIView(frame: generalFrame)
containerView.backgroundColor = UIColor.black;
PlaygroundPage.current.liveView = containerView
let parentView = UIView(frame: generalFrame)
containerView.addSubview(parentView)
let url = URL(string: "https://static.pexels.com/photos/168066/pexels-photo-168066-large.jpeg")
let data = try Data(contentsOf: url!);
let imageView = UIImageView(frame:parentView.bounds)
imageView.image = UIImage(data: data)
imageView.contentMode = .scaleAspectFill
let maskView = UIView(frame:parentView.bounds)
maskView.backgroundColor = UIColor.black
maskView.layer.mask = {() -> CALayer in
var roundedRect = CGRect (
x: 0.0,
y: 0.0,
width: parentView.bounds.size.width * 0.5,
height: parentView.bounds.size.width * 0.5
);
roundedRect.origin.x = parentView.frame.size.width / 2 - roundedRect.size.width / 2;
roundedRect.origin.y = parentView.frame.size.height / 2 - roundedRect.size.height / 2;
let cornerRadius = roundedRect.size.height / 2.0;
let path = UIBezierPath(rect:parentView.bounds)
let croppedPath = UIBezierPath(roundedRect: roundedRect, cornerRadius: cornerRadius)
path.append(croppedPath)
path.usesEvenOddFillRule = true
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath;
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
return maskLayer
}()
let blurView = UIBlurEffect(style: .light)
let effectView = UIVisualEffectView(effect: blurView)
effectView.frame = generalFrame
effectView.mask = maskView
parentView.addSubview(imageView)
parentView.addSubview(effectView)
I managed to get this working. The fix was to apply mask to UIVisualEffectView:
let maskLayer = CAShapeLayer()
effectView.layer.mask = maskLayer
import UIKit
import PlaygroundSupport
let generalFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let containerView = UIView(frame: generalFrame)
containerView.backgroundColor = UIColor.black;
PlaygroundPage.current.liveView = containerView
let parentView = UIView(frame: generalFrame)
containerView.addSubview(parentView)
let url = URL(string: "https://static.pexels.com/photos/168066/pexels-photo-168066-large.jpeg")
let data = try Data(contentsOf: url!);
let imageView = UIImageView(frame:parentView.bounds)
imageView.image = UIImage(data: data)
imageView.contentMode = .scaleAspectFill
var roundedRect = CGRect (
x: 0.0,
y: 0.0,
width: parentView.bounds.size.width * 0.5,
height: parentView.bounds.size.width * 0.5
);
roundedRect.origin.x = parentView.frame.size.width / 2 - roundedRect.size.width / 2;
roundedRect.origin.y = parentView.frame.size.height / 2 - roundedRect.size.height / 2;
let cornerRadius = roundedRect.size.height / 2.0;
let path = UIBezierPath(rect:parentView.bounds)
let croppedPath = UIBezierPath(roundedRect: roundedRect, cornerRadius: cornerRadius)
path.append(croppedPath)
path.usesEvenOddFillRule = true
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath;
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
let blurView = UIBlurEffect(style: .light)
let effectView = UIVisualEffectView(effect: blurView)
effectView.frame = generalFrame
effectView.layer.mask = maskLayer
parentView.addSubview(imageView)
parentView.addSubview(effectView)

image disappears after keyframe animation

I have an animation where a line is drawn and then with that another image moves. The animation works good but as the animation completes the image disappears from the view. The image should stay after completion of the animation. My code for the animation:
private func addPlaneAnimation() {
let image = UIImageView(frame: CGRect(x: -30, y: view.height*0.8, width: 30, height: 30))
image.image = R.image.plane()
view.addSubview(image)
let path = UIBezierPath()
path.move(to: image.center)
path.addLine(to: CGPoint(x: (view.width*0.85), y: (view.height*0.40)))
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayer.strokeColor = R.color.Purple()?.cgColor
shapeLayer.lineWidth = 3
shapeLayer.path = path.cgPath
view.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 4
animation.beginTime = CACurrentMediaTime() + 0.2
animation.isRemovedOnCompletion = false
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.path = path.cgPath
pathAnimation.duration = 4
pathAnimation.autoreverses = false
pathAnimation.isRemovedOnCompletion = false
shapeLayer.add(animation, forKey: "MyAnimation")
image.layer.add(pathAnimation, forKey: "position")
view.bringSubviewToFront(cardBarcode)
}
Can someone please help me here?
Just add two more properties.
For strokeEnd animation
animation.fillMode = CAMediaTimingFillMode.backwards
and for pathAnimation
pathAnimation.fillMode = CAMediaTimingFillMode.forwards

How to animate the path of a UIView's mask?

I have been exploring this questions through playgrounds for days now without finding any solution.
I am been managing to create a hole in a blurred UIView. Now I am wondering how to animate the radius of this circle, ergo animating the blurred view's mask's path.
Here is my code so far:
import Foundation
import UIKit
import PlaygroundSupport
let contentRect = CGRect(x: 0, y: 0, width: 400, height: 400)
let contentView = UIView(frame: contentRect)
contentView.backgroundColor = .white
PlaygroundPage.current.liveView = contentView
let image = UIImage(named: "landscape.jpg")
let imageView = UIImageView(frame: contentRect, image: image)
imageView.contentMode = .scaleAspectFill
let blur = UIBlurEffect(style: .light)
let blurView = UIVisualEffectView(effect: blur)
blurView.frame = contentRect
let path = UIBezierPath(rect: contentRect)
let circle = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 300, height: 300))
path.append(circle)
path.usesEvenOddFillRule = true
let toPath = UIBezierPath(rect: contentRect)
let toCircle = UIBezierPath(ovalIn: CGRect(x: 150, y: 150, width: 100, height: 100))
toPath.append(toCircle)
toPath.usesEvenOddFillRule = true
let hole = CAShapeLayer()
hole.path = path.cgPath
hole.fillColor = UIColor.green.cgColor
hole.fillRule = kCAFillRuleEvenOdd
let mask = UIView(frame: contentRect)
contentView.addSubview(imageView)
contentView.addSubview(blurView)
mask.layer.addSublayer(hole)
blurView.mask = mask
I have been trying this, without success:
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = hole.path
animation.toValue = toPath.cgPath
animation.duration = 2
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
hole.add(animation, forKey: nil)
CATransaction.begin()
CATransaction.disableActions()
hole.path = toPath.cgPath
CATransaction.commit()
Also tried UIView.animate(... or extracting blurView.layer.sublayers to add them to my contentView, but it just gets messy.
Another approach would be to use a CGAffineTransform, which I tried, but the blur gets disrupted. Plus I have trouble to use a workaround when I feel so close to a solution.
Any help would be greatly appreciated! Thanks!

Animating Background Color Change - Side to Side

Let's say I have a rectangle that is horizontally long. It's blue. I want it to be red. However, I want the color change to start on one side and end on the other.
I can use a key frame animation to make the whole view gradually change from red to blue. Is there a way to gradually change it from left-right/right-left??
UIView.animateKeyframesWithDuration(2.0 /*Total*/, delay: 0.0, options: UIViewKeyframeAnimationOptions.CalculationModeLinear, animations: {
UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 1/1, animations:{
self.view.backgroundColor = UIColor.redColor()
self.view.layoutIfNeeded()
})
},
completion: { finished in
if (!finished) { return }
})
Take a look at CAGradientLayer. You can animate its locations, colors or endPoint and startPoint
Here is a quick snippet you can paste into playground and see how it can work. In this case I'm animating the gradients color locations.
import UIKit
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 400))
let startLocations = [0, 0]
let endLocations = [1, 2]
let layer = CAGradientLayer()
layer.colors = [UIColor.redColor().CGColor, UIColor.blueColor().CGColor]
layer.frame = view.frame
layer.locations = startLocations
layer.startPoint = CGPoint(x: 0.0, y: 1.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
view.layer.addSublayer(layer)
let anim = CABasicAnimation(keyPath: "locations")
anim.fromValue = startLocations
anim.toValue = endLocations
anim.duration = 2.0
layer.addAnimation(anim, forKey: "loc")
layer.locations = endLocations
XCPlaygroundPage.currentPage.liveView = view
Swift 3 version:
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 400))
let startLocations = [0, 0]
let endLocations = [1, 2]
let layer = CAGradientLayer()
layer.colors = [UIColor.red.cgColor, UIColor.blue.cgColor]
layer.frame = view.frame
layer.locations = startLocations as [NSNumber]?
layer.startPoint = CGPoint(x: 0.0, y: 1.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
view.layer.addSublayer(layer)
let anim = CABasicAnimation(keyPath: "locations")
anim.fromValue = startLocations
anim.toValue = endLocations
anim.duration = 2.0
layer.add(anim, forKey: "loc")
layer.locations = endLocations as [NSNumber]?
PlaygroundPage.current.liveView = view

Trouble aligning a UIView and a CAShapeLayer

I'm having trouble aligning a UIView and a CAShapeLayer.
Here is sample code demonstrating the issue:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
square.transform = CGAffineTransformMakeRotation(CGFloat(20*M_PI/180))
square.backgroundColor = UIColor.grayColor()
view.addSubview(square)
let strokeLayer = CAShapeLayer()
strokeLayer.path = UIBezierPath(rect: square.bounds).CGPath
strokeLayer.position = square.center
strokeLayer.setAffineTransform(square.transform)
strokeLayer.fillColor = UIColor.clearColor().CGColor
strokeLayer.strokeColor = UIColor.redColor().CGColor
strokeLayer.lineWidth = 1
view.addSubview(square)
view.layer.addSublayer(strokeLayer)
}
}
Here's an image of the result:
How do I get the two to align?
The only change you would need to make is to add:
strokeLayer.frame = square.layer.bounds
Right after:
let strokeLayer = CAShapeLayer()
I.e.,
let strokeLayer = CAShapeLayer()
strokeLayer.frame = square.layer.bounds
strokeLayer.path = UIBezierPath(rect: square.bounds).CGPath
strokeLayer.position = square.center
strokeLayer.setAffineTransform(square.transform)
strokeLayer.fillColor = UIColor.clearColor().CGColor
strokeLayer.strokeColor = UIColor.redColor().CGColor
strokeLayer.lineWidth = 1
What if you position your shape layer at (0,0)
strokeLayer.position = CGPointMake( 0, 0 )

Resources