CAKeyframeAnimation not working in nib - ios

I have a nib file that has to animate a view to move in a circular path.
But when I run my code I see the view was not animated and at the top corner of my view
this is my code
override func awakeFromNib() {
let circlePath = UIBezierPath(arcCenter: CGPointMake(CGRectGetMidX(loadingview.bounds), CGRectGetMidY(loadingview.bounds)), radius: 30, startAngle: 0, endAngle:CGFloat(M_PI)*2, clockwise: true)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.beginTime = 0.0
animation.duration = 1
animation.repeatCount = Float.infinity
animation.path = circlePath.CGPath
let squareView = UIView()
squareView.frame = CGRectMake(0, 0, 10, 10);
squareView.backgroundColor = UIColor.grayColor()
self.addSubview(squareView)
// You can also pass any unique string value for key
// circleLayer is only used to locate the circle animation path
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.CGPath
circleLayer.strokeColor = UIColor.blackColor().CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
loadingview.layer.addSublayer(circleLayer)
squareView.layer.addAnimation(animation, forKey: "position")
self.layer.shadowColor = UIColor.blackColor().CGColor
self.layer.shadowOpacity = 1
self.layer.shadowOffset = CGSize.zero
self.layer.shadowRadius = 1
self.layer.cornerRadius = 10.0
}

You cannot begin an animation in awakeFromNib. It is too soon. You can only animate a layer that is actually in your interface's view/layer hierarchy, and a layer of a view that has just come out of the nib, like loadingview, has not been placed into the full interface yet.
Also, this would appear to be a CABasicAnimation, not a CAKeyframeAnimation. You need to fix that.

You have to load the xib first, then in the ViewController in viewDidAppear() method just call yourXib.awakeFromNib()
yourXib = Bundle.main.loadNibNamed("yourXib", owner: self, options: nil)?.first as! UIView
yourXib.awakeFromNib()

Related

How to animate object using bezier path?

Here is my code to draw dashed line
func drawLine() {
let shapeLayer = CAShapeLayer()
shapeLayer.bounds = viewDraw.bounds
shapeLayer.position = CGPoint(x: viewDraw.bounds.width/2, y: viewDraw.bounds.height/2)
shapeLayer.fillColor = nil
shapeLayer.strokeColor = ColorConstants.ThemeColor.cgColor
shapeLayer.lineWidth = 3
shapeLayer.lineJoin = CAShapeLayerLineJoin.round // Updated in swift 4.2
shapeLayer.lineDashPattern = [10]
let path = UIBezierPath(roundedRect: viewDraw.bounds, cornerRadius: viewDraw.bounds.size.height/2)
shapeLayer.path = path.cgPath
viewDraw.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 1
shapeLayer.add(animation, forKey: "MyAnimation")
}
Now as per my screenshot I want to animate blue dot to red dot within that dashed line infinite time. When blue dot reaches red dot then it will start again with blue dot points.
Note: This whole screen is dynamic so, Line draw is also dynamic according to screen height
Any help would be appreciated
You are almost there
Now you have to add animation using CAKeyframeAnimation which has property path where you can pass the path where you want to animate
here is working sample code
func drawLine() {
let shapeLayer = CAShapeLayer()
shapeLayer.bounds = self.view.bounds
shapeLayer.position = CGPoint(x: self.view.bounds.width/2, y: self.view.bounds.height/2)
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 3
shapeLayer.lineJoin = CAShapeLayerLineJoin.round // Updated in swift 4.2
shapeLayer.lineDashPattern = [10]
let path = UIBezierPath(roundedRect: self.view.bounds, cornerRadius: self.view.bounds.size.height/2)
shapeLayer.path = path.cgPath
self.view.layer.addSublayer(shapeLayer)
let animation1 = CABasicAnimation(keyPath: "strokeEnd")
animation1.fromValue = 0
animation1.duration = 1
shapeLayer.add(animation1, forKey: "MyAnimation")
// Create squareView and add animation
let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation.duration = 1
animation.repeatCount = MAXFLOAT
animation.path = path.cgPath
let squareView = UIView()
// Don't worry about x,y and width and height it will not affect the animation
squareView.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
squareView.backgroundColor = .lightGray
view.addSubview(squareView)
// You can also pass any unique string value for key
squareView.layer.add(animation, forKey: nil)
}
You can see this tutorial for more help http://mathewsanders.com/animations-in-swift-part-two/

Center of CAGradientLayer returns wrong position on rotate animation

I have a circle like in Instagram profile image which has story. I want to have an affect like circle is spinning. For that, I used CABasicAnimation. It is spinning but not in center.
As I searched, I need to give bounty for shapeLayer but when I do that, It doesn't placed where It needs to be.
How can I achieve animation like in Instagram story circle (like circle is spinning)?
EDIT I also try to add "colors" animation but because It works like It is in square, I can't get the desired result.
func addCircle() {
let circularPath = UIBezierPath(arcCenter: CGPoint(x: self.bounds.midX, y: self.bounds.midY), radius: self.bounds.width / 2, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 1.0
gradient.frame = circularPath.bounds
gradient.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
shapeLayer.path = circularPath.cgPath
gradient.mask = shapeLayer
self.layer.addSublayer(gradient)
}
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 4
animation.repeatCount = MAXFLOAT
return animation
}()
#objc private func handleTap() {
print("Attempting to animate stroke")
shapeLayer.add(rotationAnimation, forKey: "urSoBasic2")
}
If spinning, why not spin gradient
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = Float.pi * 2
animation.duration = 4
animation.repeatCount = MAXFLOAT
return animation
}()
#objc func handleTap() {
print("Attempting to animate stroke")
gradient.add(rotationAnimation, forKey: "urSoBasic2")
}
As to off center problem. The correct frame for gradient should be like this :
gradient.frame = self.bounds
To verify the center, you may add a background for the view:
self.background = UIColor.black.
The reason is width and height of the view is not set well due to the constraints. Try to set the correct bounds of the view, so when you add the circularPath, it's center in the view.

CALayer circle mask animation incorrect behaviour + Swift

I have been trying to do this animation for a couple of days now. I am trying to animate the size of my mask, basically taking a large circle (mask) and shrinking it.
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animateMask()
}
func presentMaskScreenWithAnimation () {
//Setup Mask Layer
let bounds = myView.bounds
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.fillColor = UIColor.brown.cgColor
//CropCircle Out of mask Layer
let initialCircle = CGRect(x: 10, y: 10, width: 300, height: 300)
let path = UIBezierPath(roundedRect: initialCircle, cornerRadius: initialCircle.size.width/2)
path.append(UIBezierPath(rect: bounds))
maskLayer.path = path.cgPath
maskLayer.fillRule = kCAFillRuleEvenOdd
myView.layer.mask = maskLayer
//Define new circle path
let circlePath2 = CGRect(x: 50, y: 50, width: 100, height: 100)
let path2 = UIBezierPath(roundedRect: circlePath2, cornerRadius: circlePath2.size.width/2)
//Create animation
let anim = CABasicAnimation(keyPath: "path")
anim.fromValue = path.cgPath
anim.toValue = path2.cgPath
anim.duration = 3.0
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
maskLayer.add(anim, forKey: nil)
}
This code sets up the mask and begins to animate it however it inverts the entire maskLayer, I am trying to just animate the path of the circle, Large -> Small
Start
Finished
Add path2.append(UIBezierPath(rect: bounds)) after let path2 = UIBezierPath(roundedRect: circlePath2, cornerRadius: circlePath2.size.width/2). Also do not forget to do maskLayer.path = path2.cgPath at the end, if you want the shrinking mask stays after animation.

Incorrect position of CAShapeLayer

I have a UIView called viewProgress. It is the white box in the image. I want a circular progress bar, which is the green circle in the image. The progress bar should stay within the white box, but as you can see it is way off.
How do I make it stay inside the viewProgress?
Here you have my animation function:
func animateView() {
let circle = viewProgress // viewProgress is a UIView
var progressCircle = CAShapeLayer()
let circlePath = UIBezierPath(arcCenter: circle.center, radius: circle.bounds.midX, startAngle: -CGFloat(M_PI_2), endAngle: CGFloat(3.0 * M_PI_2), clockwise: true)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.whiteColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 10.0
circle.layer.addSublayer(progressCircle)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 0.4
animation.duration = 1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
progressCircle.addAnimation(animation, forKey: "ani")
}
And I'm just calling the function inside viewDidLoad()
Basically u have few problems:
incorrect position of your layer - frame should be equal to bounds of parent layer in parentView
you animation will play only once - if u want to make it infinite add something like animation.repeatCount = MAXFLOAT
to make sure that all size correct - in layoutSubviews recreate layers, with removing previously created one
UIBezierPath(arcCenter... will start path from rightSide, u can also use simpler variant like UIBezierPath(ovalInRect. With this in mind u also need to rotate layer for M_PI_2 if u want to make start of drawing in the very top of circle
U should also take in mind that with line width 10, half of u'r circle path will be catted by clipSubviews of parentView. To prevent this use modified parentLayer rect
U can also maybe want to improve visualization of lineCap, buy setting it to "round" style
progressCircle.addAnimation(animation, forKey: "ani") key for anim not required if u dont want to get completion event from animation - i your case i see there is no delegate and no removeOnCompletion flag setted to false, so make assumption that u don`t actually need it
Keeping this all in mind code can be like :
let circle = UIView(frame: CGRectMake(100, 100, 100, 100)) // viewProgress is a UIView
circle.backgroundColor = UIColor.greenColor()
view.addSubview(circle)
var progressCircle = CAShapeLayer()
progressCircle.frame = view.bounds
let lineWidth:CGFloat = 10
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 = 10.0
progressCircle.frame = view.bounds
progressCircle.lineCap = "round"
circle.layer.addSublayer(progressCircle)
circle.transform = CGAffineTransformRotate(circle.transform, CGFloat(-M_PI_2))
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1.1
animation.duration = 1
animation.repeatCount = MAXFLOAT
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
progressCircle.addAnimation(animation, forKey: nil)
and result actually like:
Note:
If u want to animate rotation of animated layer u should also add additional animation in to anim group

Bezier animation to reveal and conceal a checkmark with Swift

In my app I have a checkmark inside a UIView which I reveal or conceal depending on the button the user clicks on to create the effect that the checkmark is being drawn or erased. I am using Bezier path with a mask to animate the checkmark being revealed or concealed with the code shown below. However, in the code to conceal it, the checkmark is being concealed from right to left and I would like to do it from left to right. What am I missing?
Code to reveal the checkmark
func checkmarkAnimationToReveal(view: UIView) {
let path = UIBezierPath(rect: CGRectMake(0, 0, view.bounds.width / 10, view.bounds.height))
let mask = CAShapeLayer()
mask.anchorPoint = CGPointMake(0.5, 1.0)
mask.path = path.CGPath
view.layer.mask = mask
let anim = CABasicAnimation(keyPath: "path")
anim.fromValue = path.CGPath
anim.toValue = UIBezierPath(rect:view.bounds).CGPath
anim.duration = 1.0
anim.fillMode = kCAFillModeForwards
anim.removedOnCompletion = false
view.layer.mask!.addAnimation(anim, forKey: "anim")
}
Code to conceal the checkmark
func checkmarkAnimationToConceal(view: UIView) {
CATransaction.begin()
CATransaction.setCompletionBlock({
print("completion reached")
self.checkmarkLabel.hidden = true
})
let path = UIBezierPath(rect: CGRectMake(0, 0, view.bounds.width / 10, view.bounds.height))
let mask = CAShapeLayer()
mask.anchorPoint = CGPointMake(0.5, 1.0)
mask.path = path.CGPath
view.layer.mask = mask
let anim = CABasicAnimation(keyPath: "path")
anim.fromValue = UIBezierPath(rect:view.bounds).CGPath
anim.toValue = path.CGPath
anim.duration = 1.0
anim.fillMode = kCAFillModeRemoved
anim.removedOnCompletion = false
view.layer.mask!.addAnimation(anim, forKey: "anim")
}

Resources