Spinning imageview animation - ios

I want to spin a imageview for forever. I tried following code:
UIView.animateWithDuration(3, animations: {
self.loginLogo.transform = CGAffineTransformMakeRotation((360 * CGFloat(M_PI)) / 360)
}){ (finished) -> Void in
self.rotateImage()
}
But this is working for one time. My imageview is not spinning forever. How can I fix it?

If you want to rotate your image forever you can do it this way:
func rotateViewLayer() {
let rotateView = CABasicAnimation()
rotateView.fromValue = 0.degreesToRadian
rotateView.toValue = 360.degreesToRadian
rotateView.duration = 1
rotateView.repeatCount = Float.infinity
rotateView.removedOnCompletion = false
rotateView.fillMode = kCAFillModeForwards
rotateView.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
imageView.layer.addAnimation(rotateView, forKey: "transform.rotation.z")
}
And here is your helper extension:
extension Int {
var degreesToRadian : CGFloat {
return CGFloat(self) * CGFloat(M_PI) / 180.0
}
}
And you can refer THIS sample project for more Info.

use this on layer of your view
var myImageView:UIImageView?
var rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation.fromValue = 0.0
rotation.toValue = 2*M_PI
rotation.duration = 1.1
rotation.repeatCount = Float.infinity
myImageView?.layer.addAnimation(rotation, forKey: "Spin")

This should work
func startAnimationWithDuration(duration:CGFloat)
{
var rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.fromValue = 0
rotationAnimation.toValue = 2 * M_PI
rotationAnimation.duration = NSTimeInterval(duration)
rotationAnimation.repeatCount = 1000.0 //some large value
imageView.layer.addAnimation(rotationAnimation, forKey: "spin")
}

Related

Resume CABasicAnimation backwards by setting .speed equal to -1

EDIT: i've refactored the question a bit and solved part of the issue, now the question comes down to why does the presentation layer glitches/flashes when the animation is resumed. At this point tho i'm accepting any answer that makes the animation resume both forwards and backwards at will with no issue. I'm not sure the approach i'm using is the right one, i'm still pretty new to Swift.
Note: Sample project at the bottom, for having a better understanding of the issue.
In my project i'm pausing a CABasicAnimation by setting the layer .speed property to 0, then i'm changing the animation value interactively by setting the layer's .timeOffset property equal to a UISlider .value property whenever the user scrolls the slider. By code:
layer.speed = 0
Then when the user slides:
layer.timeOffset = CFTimeInterval(sender.value)
Now i want to resume the animation backwards or forwards at will whenever the user gesture on the slider ends, so from the starting point related to the current animation value. The only viable solution i've found which runs smoothly is the following, but it works only going forwards:
let pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
Then i can simply pause it again at the completion of the animation:
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
layer.timeOffset = 0
layer.speed = 0
}
From my understanding, .speed not only defines the actual speed of the animation combined with the .duration property, but also the direction of the animation: if i set a layer's speed equal to -1 then the animation completes backwards. Referring to this answer in regards to how CAMediaTiming works, i was trying to change the up above snippet's parameters to resume the animation going backwards with no luck. I thought this would work:
let pausedTime = layer.timeOffset
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
layer.timeOffset = pausedTime*2
layer.speed = -1.0
but the layer is never animated like so. The issue seems to be related to the convertTime method.
Then i found this question which is basically the same of mine, and the only answer has a decent solution. Refactoring a bit the code, i can just say:
layer.beginTime = CACurrentMediaTime()
layer.speed = -1
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
layer.timeOffset = 0
layer.speed = 0
}
However, when the animation is played backwards is very glitchy, in particular the presentation layer flashes both at the resume and on completion. I've tried various solutions with no luck, some speculations i've made:
it may be an issue related to CAMediaTimingFillMode, as i can set it .back or .forwards but when it resumes the animation is neither in it's final nor in it's initial state and thus the initial frame is not rendered;
it is caused by the fact that i'm not keeping the modal tree and the presentation tree synchronized.
Both of these however doesn't explain while it flickers/flashes both on resume and on completion. Additionally, it seems to me that the animation may have a duration of 1 when resumed forwards, but only of 1-timeOffset when resumed backwards, not sure tho.
Really not sure what's the actual problem and how to fix this mess. All suggestions are more than welcomed.
For anyone interested, here's a sample project similar to mine, inspired by another question (animation is running forward, to run it backwards and catch the glitch just call resumeLayerBackwards()). I know the code should be refactored, but still for the purpose it's fine. Just copy, paste and run:
import UIKit
class ViewController: UIViewController {
var perspectiveLayer: CALayer = {
let perspectiveLayer = CALayer()
perspectiveLayer.speed = 0.0
return perspectiveLayer
}()
var mainView: UIView = {
let view = UIView()
return view
}()
private let slider: UISlider = {
let slider = UISlider()
slider.addTarget(self, action: #selector(slide(sender:event:)) , for: .valueChanged)
return slider
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(slider)
animate()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
slider.frame = CGRect(x: view.bounds.size.width/3,
y: view.bounds.size.height/10*8,
width: view.bounds.size.width/3,
height: view.bounds.size.height/10)
}
#objc private func slide(sender: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
switch touchEvent.phase {
case .ended:
resumeLayer(layer: perspectiveLayer)
default:
perspectiveLayer.timeOffset = CFTimeInterval(sender.value)
}
}
}
private func resumeLayer(layer: CALayer) {
let pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
layer.timeOffset = 1.0
layer.speed = 0.0
}
}
private func resumeLayerBackwards(layer: CALayer) {
layer.beginTime = CACurrentMediaTime()
layer.speed = -1
DispatchQueue.main.asyncAfter(deadline: .now()+1) {
layer.timeOffset = 0
layer.speed = 0
}
}
private func animate() {
var transform:CATransform3D = CATransform3DIdentity
var topSleeve:CALayer
var middleSleeve:CALayer
var bottomSleeve:CALayer
var topShadow:CALayer
var middleShadow:CALayer
let width:CGFloat = 300
let height:CGFloat = 150
var firstJointLayer:CALayer
var secondJointLayer:CALayer
mainView = UIView(frame:CGRect(x: 50, y: 50, width: width, height: height*3))
mainView.backgroundColor = UIColor.yellow
view.addSubview(mainView)
perspectiveLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
mainView.layer.addSublayer(perspectiveLayer)
firstJointLayer = CATransformLayer()
firstJointLayer.frame = mainView.bounds
perspectiveLayer.addSublayer(firstJointLayer)
topSleeve = CALayer()
topSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
topSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
topSleeve.backgroundColor = UIColor.red.cgColor
topSleeve.position = CGPoint(x: width/2, y: 0)
firstJointLayer.addSublayer(topSleeve)
topSleeve.masksToBounds = true
secondJointLayer = CATransformLayer()
secondJointLayer.frame = mainView.bounds
secondJointLayer.frame = CGRect(x: 0, y: 0, width: width, height: height*2)
secondJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
secondJointLayer.position = CGPoint(x: width/2, y: height)
firstJointLayer.addSublayer(secondJointLayer)
middleSleeve = CALayer()
middleSleeve.frame = CGRect(x: 0, y: 0, width: width, height: height)
middleSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
middleSleeve.backgroundColor = UIColor.blue.cgColor
middleSleeve.position = CGPoint(x: width/2, y: 0)
secondJointLayer.addSublayer(middleSleeve)
middleSleeve.masksToBounds = true
bottomSleeve = CALayer()
bottomSleeve.frame = CGRect(x: 0, y: height, width: width, height: height)
bottomSleeve.anchorPoint = CGPoint(x: 0.5, y: 0)
bottomSleeve.backgroundColor = UIColor.gray.cgColor
bottomSleeve.position = CGPoint(x: width/2, y: height)
secondJointLayer.addSublayer(bottomSleeve)
firstJointLayer.anchorPoint = CGPoint(x: 0.5, y: 0)
firstJointLayer.position = CGPoint(x: width/2, y: 0)
topShadow = CALayer()
topSleeve.addSublayer(topShadow)
topShadow.frame = topSleeve.bounds
topShadow.backgroundColor = UIColor.black.cgColor
topShadow.opacity = 0
middleShadow = CALayer()
middleSleeve.addSublayer(middleShadow)
middleShadow.frame = middleSleeve.bounds
middleShadow.backgroundColor = UIColor.black.cgColor
middleShadow.opacity = 0
transform.m34 = -1/700
perspectiveLayer.sublayerTransform = transform
var animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = -90*Double.pi/180
firstJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = 180*Double.pi/180
secondJointLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "transform.rotation.x")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = -160*Double.pi/180
bottomSleeve.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "bounds.size.height")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = perspectiveLayer.bounds.size.height
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "position.y")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = perspectiveLayer.position.y
animation.toValue = 0
perspectiveLayer.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
topShadow.add(animation, forKey: nil)
animation = CABasicAnimation(keyPath: "opacity")
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
animation.fromValue = 0
animation.toValue = 0.5
middleShadow.add(animation, forKey: nil)
}
}
I managed to remove the glitch for resumeLayerBackwards(layer:) in the sample project.
Two problems there in fact:
there is an empty screen after animation has visually finished
the empty screen is visible for 1 - .timeOffset seconds
So, seems like the problem is that animation in fact plays not just for .timeOffset period, but for the whole .duration period. And the empty screen appears because there is no animation defined for 1 - .timeOffset block.
Just to recall: CALayer also adopts CAMediaTiming protocol, as CAAnimation does (with all the properties defined: although some of them seem not be very clear how to be applied to a layer).
With speed = -1 after .timeOffset seconds passed — the property .timeOffset becomes equal to zero. It means that animation has reached its beginning and therefore (with negative speed) it is finished. Though it is not that obvious — seems like it is removed because of the .fillMode property. To fix this I've added perspectiveLayer.fillMode = .forwards to animate() method.
To have animation completed exactly after .timeOffset seconds instead of the whole .duration — use .repeatDuration property. I've added layer.repeatDuration = layer.timeOffset to your resumeLayerBackwards(layer:) method.
The project works only with both lines added.
I can't say that the solution is really logical for me, although it works. Negative speed works a bit unpredictable as for me. In my project I used to reverse animation by swapping begin and end values in cloned animation object.

How to shake the text field in swift 4? [duplicate]

I am trying to figure out how to make the text Field shake on button press when the user leaves the text field blank.
I currently have the following code working:
if self.subTotalAmountData.text == "" {
let alertController = UIAlertController(title: "Title", message:
"What is the Sub-Total!", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Okay", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
} else {
}
But i think it would be much more appealing to just have the text field shake as an alert.
I can't find anything to animate the text field.
Any ideas?
Thanks!
You can change the duration and repeatCount and tweak it. This is what I use in my code. Varying the fromValue and toValue will vary the distance moved in the shake.
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: viewToShake.center.x - 10, y: viewToShake.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: viewToShake.center.x + 10, y: viewToShake.center.y))
viewToShake.layer.add(animation, forKey: "position")
The following function is used in any view.
extension UIView {
func shake() {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.6
animation.values = [-20.0, 20.0, -20.0, 20.0, -10.0, 10.0, -5.0, 5.0, 0.0 ]
layer.add(animation, forKey: "shake")
}
}
EDIT: using CABasicAnimation cause the app to crash if you ever trigger the animation twice in a row. So be sure to use CAKeyframeAnimation. Bug has been fixed, thanks to the comments :)
Or you can use this if you want more parameters (in swift 5) :
public extension UIView {
func shake(count : Float = 4,for duration : TimeInterval = 0.5,withTranslation translation : Float = 5) {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.repeatCount = count
animation.duration = duration/TimeInterval(animation.repeatCount)
animation.autoreverses = true
animation.values = [translation, -translation]
layer.add(animation, forKey: "shake")
}
}
You can call this function on any UIView, UIButton, UILabel, UITextView etc. This way
yourView.shake()
Or this way if you want to add some custom parameters to the animation:
yourView.shake(count: 5, for: 1.5, withTranslation: 10)
I think all of these are dangerous.
If your shake animation is based on a user action and that user action is triggered while animating.
CRAAAAAASH
Here is my way in Swift 4:
static func shake(view: UIView, for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
view.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
view.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.startAnimation()
}
Maybe not the cleanest, but this method can be triggered repeatedly and is easily understood
Edit:
I am a huge proponent for usage of UIViewPropertyAnimator. So many cool features that allow you to make dynamic modifications to basic animations.
Here is another example to add a red border while the view is shaking, then removing it when the shake finishes.
static func shake(view: UIView, for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
view.layer.borderColor = UIColor.red.cgColor
view.layer.borderWidth = 1
view.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
view.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.addCompletion { (_) in
view.layer.borderWidth = 0
}
propertyAnimator.startAnimation()
}
Swift 5.0
extension UIView {
func shake(){
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 3
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 10, y: self.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 10, y: self.center.y))
self.layer.add(animation, forKey: "position")
}
}
To use
self.vwOffer.shake()
Swift 5
Safe (non crash) shake extension for Corey Pett answer:
extension UIView {
func shake(for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
self.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.startAnimation()
}
}
extension CALayer {
func shake(duration: NSTimeInterval = NSTimeInterval(0.5)) {
let animationKey = "shake"
removeAnimationForKey(animationKey)
let kAnimation = CAKeyframeAnimation(keyPath: "transform.translation.x")
kAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
kAnimation.duration = duration
var needOffset = CGRectGetWidth(frame) * 0.15,
values = [CGFloat]()
let minOffset = needOffset * 0.1
repeat {
values.append(-needOffset)
values.append(needOffset)
needOffset *= 0.5
} while needOffset > minOffset
values.append(0)
kAnimation.values = values
addAnimation(kAnimation, forKey: animationKey)
}
}
How to use:
[UIView, UILabel, UITextField, UIButton & etc].layer.shake(NSTimeInterval(0.7))
I tried some of the available solutions but none of them were handling the full shake animation: moving from left to right and get back to the original position.
So, after some investigation I found the right solution that I consider to be a successful shake using UIViewPropertyAnimator.
func shake(completion: (() -> Void)? = nil) {
let speed = 0.75
let time = 1.0 * speed - 0.15
let timeFactor = CGFloat(time / 4)
let animationDelays = [timeFactor, timeFactor * 2, timeFactor * 3]
let shakeAnimator = UIViewPropertyAnimator(duration: time, dampingRatio: 0.3)
// left, right, left, center
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 20, y: 0)
})
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: -20, y: 0)
}, delayFactor: animationDelays[0])
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 20, y: 0)
}, delayFactor: animationDelays[1])
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: animationDelays[2])
shakeAnimator.startAnimation()
shakeAnimator.addCompletion { _ in
completion?()
}
shakeAnimator.startAnimation()
}
Final result:
func shakeTextField(textField: UITextField)
{
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 3
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: textField.center.x - 10, y: textField.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: textField.center.x + 10, y: textField.center.y))
textField.layer.add(animation, forKey: "position")
textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "",
attributes: [NSAttributedStringKey.foregroundColor: UIColor.red])
}
//write in base class or any view controller and use it
This is based on CABasicAnimation, it contain also an audio effect :
extension UIView{
var audioPlayer = AVAudioPlayer()
func vibrate(){
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 5.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 5.0, self.center.y))
self.layer.addAnimation(animation, forKey: "position")
// audio part
do {
audioPlayer = try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(mySoundFileName, ofType: "mp3")!))
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print("∙ Error playing vibrate sound..")
}
}
}
func addShakeAnimation(duration: CGFloat = 0.3, repeatCount: Float = 4, angle: Float = Float.pi / 27, completion: (() -> Void)? = nil) {
let rotationAnimation = CABasicAnimation.init(keyPath: "transform.rotation.z")
rotationAnimation.duration = TimeInterval(duration/CGFloat(repeatCount))
rotationAnimation.repeatCount = repeatCount
rotationAnimation.autoreverses = true
rotationAnimation.fromValue = -angle
rotationAnimation.toValue = angle
rotationAnimation.isRemovedOnCompletion = true
CATransaction.begin()
CATransaction.setCompletionBlock {
if let completion = completion {
completion()
}
}
layer.add(rotationAnimation, forKey: "shakeAnimation")
CATransaction.commit()
}

Swift 3 animation issue

I am implementing progress view that simply animate colors.
Everything is working fine exept "strokeEnd" animation that is filling by shape and making it to flicker.
Video resource
Code example:
class MaterialCircularProgressView: UIView {
let circularLayer = CAShapeLayer()
let googleColors = [
UIColor(red:0.282, green:0.522, blue:0.929, alpha:1),
UIColor(red:0.859, green:0.196, blue:0.212, alpha:1),
UIColor(red:0.957, green:0.761, blue:0.051, alpha:1),
UIColor(red:0.235, green:0.729, blue:0.329, alpha:1)
]
let inAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
return animation
}()
let outAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "strokeStart")
animation.beginTime = 0.5
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
return animation
}()
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0.0
animation.toValue = 2 * M_PI
animation.duration = 2.0
animation.repeatCount = MAXFLOAT
return animation
}()
var colorIndex : Int = 0
override init(frame: CGRect) {
super.init(frame: frame)
circularLayer.lineWidth = 4.0
circularLayer.fillColor = nil
layer.addSublayer(circularLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 2 - circularLayer.lineWidth / 2
let arcPath = UIBezierPath(arcCenter: CGPoint.zero, radius: radius, startAngle: CGFloat(M_PI_2), endAngle: CGFloat(M_PI_2 + (2 * M_PI)), clockwise: true)
circularLayer.position = center
circularLayer.path = arcPath.cgPath
animateProgressView()
circularLayer.add(rotationAnimation, forKey: "rotateAnimation")
}
func animateProgressView() {
circularLayer.removeAnimation(forKey: "strokeAnimation")
circularLayer.strokeColor = googleColors[colorIndex].cgColor
let strokeAnimationGroup = CAAnimationGroup()
strokeAnimationGroup.duration = 1.0 + outAnimation.beginTime
strokeAnimationGroup.repeatCount = 1
strokeAnimationGroup.animations = [inAnimation, outAnimation]
strokeAnimationGroup.delegate = self
circularLayer.add(strokeAnimationGroup, forKey: "strokeAnimation")
colorIndex += 1;
colorIndex = colorIndex % googleColors.count;
}
}
extension MaterialCircularProgressView: CAAnimationDelegate{
override func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if(flag) {
animateProgressView()
}
}
}
You can fix that flickering by setting strokeStart to the final value of your outAnimation in the initializer:
circularLayer.strokeStart = 1.0
The reason is that actual value of property animated by CAPropertyAnimation is not automatically set to the toValue of animation, so it reverts to it's original value which is 0.0 in your case. That's why a full circle appears for some time.
However there is a delay before next circle appears, not sure if that was your intention or another bug to be fixed.

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

Shake Animation for UITextField/UIView in Swift

I am trying to figure out how to make the text Field shake on button press when the user leaves the text field blank.
I currently have the following code working:
if self.subTotalAmountData.text == "" {
let alertController = UIAlertController(title: "Title", message:
"What is the Sub-Total!", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Okay", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
} else {
}
But i think it would be much more appealing to just have the text field shake as an alert.
I can't find anything to animate the text field.
Any ideas?
Thanks!
You can change the duration and repeatCount and tweak it. This is what I use in my code. Varying the fromValue and toValue will vary the distance moved in the shake.
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: viewToShake.center.x - 10, y: viewToShake.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: viewToShake.center.x + 10, y: viewToShake.center.y))
viewToShake.layer.add(animation, forKey: "position")
The following function is used in any view.
extension UIView {
func shake() {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.6
animation.values = [-20.0, 20.0, -20.0, 20.0, -10.0, 10.0, -5.0, 5.0, 0.0 ]
layer.add(animation, forKey: "shake")
}
}
EDIT: using CABasicAnimation cause the app to crash if you ever trigger the animation twice in a row. So be sure to use CAKeyframeAnimation. Bug has been fixed, thanks to the comments :)
Or you can use this if you want more parameters (in swift 5) :
public extension UIView {
func shake(count : Float = 4,for duration : TimeInterval = 0.5,withTranslation translation : Float = 5) {
let animation = CAKeyframeAnimation(keyPath: "transform.translation.x")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.repeatCount = count
animation.duration = duration/TimeInterval(animation.repeatCount)
animation.autoreverses = true
animation.values = [translation, -translation]
layer.add(animation, forKey: "shake")
}
}
You can call this function on any UIView, UIButton, UILabel, UITextView etc. This way
yourView.shake()
Or this way if you want to add some custom parameters to the animation:
yourView.shake(count: 5, for: 1.5, withTranslation: 10)
I think all of these are dangerous.
If your shake animation is based on a user action and that user action is triggered while animating.
CRAAAAAASH
Here is my way in Swift 4:
static func shake(view: UIView, for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
view.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
view.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.startAnimation()
}
Maybe not the cleanest, but this method can be triggered repeatedly and is easily understood
Edit:
I am a huge proponent for usage of UIViewPropertyAnimator. So many cool features that allow you to make dynamic modifications to basic animations.
Here is another example to add a red border while the view is shaking, then removing it when the shake finishes.
static func shake(view: UIView, for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
view.layer.borderColor = UIColor.red.cgColor
view.layer.borderWidth = 1
view.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
view.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.addCompletion { (_) in
view.layer.borderWidth = 0
}
propertyAnimator.startAnimation()
}
Swift 5.0
extension UIView {
func shake(){
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 3
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 10, y: self.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 10, y: self.center.y))
self.layer.add(animation, forKey: "position")
}
}
To use
self.vwOffer.shake()
Swift 5
Safe (non crash) shake extension for Corey Pett answer:
extension UIView {
func shake(for duration: TimeInterval = 0.5, withTranslation translation: CGFloat = 10) {
let propertyAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.3) {
self.transform = CGAffineTransform(translationX: translation, y: 0)
}
propertyAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: 0.2)
propertyAnimator.startAnimation()
}
}
extension CALayer {
func shake(duration: NSTimeInterval = NSTimeInterval(0.5)) {
let animationKey = "shake"
removeAnimationForKey(animationKey)
let kAnimation = CAKeyframeAnimation(keyPath: "transform.translation.x")
kAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
kAnimation.duration = duration
var needOffset = CGRectGetWidth(frame) * 0.15,
values = [CGFloat]()
let minOffset = needOffset * 0.1
repeat {
values.append(-needOffset)
values.append(needOffset)
needOffset *= 0.5
} while needOffset > minOffset
values.append(0)
kAnimation.values = values
addAnimation(kAnimation, forKey: animationKey)
}
}
How to use:
[UIView, UILabel, UITextField, UIButton & etc].layer.shake(NSTimeInterval(0.7))
I tried some of the available solutions but none of them were handling the full shake animation: moving from left to right and get back to the original position.
So, after some investigation I found the right solution that I consider to be a successful shake using UIViewPropertyAnimator.
func shake(completion: (() -> Void)? = nil) {
let speed = 0.75
let time = 1.0 * speed - 0.15
let timeFactor = CGFloat(time / 4)
let animationDelays = [timeFactor, timeFactor * 2, timeFactor * 3]
let shakeAnimator = UIViewPropertyAnimator(duration: time, dampingRatio: 0.3)
// left, right, left, center
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 20, y: 0)
})
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: -20, y: 0)
}, delayFactor: animationDelays[0])
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 20, y: 0)
}, delayFactor: animationDelays[1])
shakeAnimator.addAnimations({
self.transform = CGAffineTransform(translationX: 0, y: 0)
}, delayFactor: animationDelays[2])
shakeAnimator.startAnimation()
shakeAnimator.addCompletion { _ in
completion?()
}
shakeAnimator.startAnimation()
}
Final result:
func shakeTextField(textField: UITextField)
{
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.07
animation.repeatCount = 3
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: textField.center.x - 10, y: textField.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: textField.center.x + 10, y: textField.center.y))
textField.layer.add(animation, forKey: "position")
textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "",
attributes: [NSAttributedStringKey.foregroundColor: UIColor.red])
}
//write in base class or any view controller and use it
This is based on CABasicAnimation, it contain also an audio effect :
extension UIView{
var audioPlayer = AVAudioPlayer()
func vibrate(){
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 5.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 5.0, self.center.y))
self.layer.addAnimation(animation, forKey: "position")
// audio part
do {
audioPlayer = try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource(mySoundFileName, ofType: "mp3")!))
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print("∙ Error playing vibrate sound..")
}
}
}
func addShakeAnimation(duration: CGFloat = 0.3, repeatCount: Float = 4, angle: Float = Float.pi / 27, completion: (() -> Void)? = nil) {
let rotationAnimation = CABasicAnimation.init(keyPath: "transform.rotation.z")
rotationAnimation.duration = TimeInterval(duration/CGFloat(repeatCount))
rotationAnimation.repeatCount = repeatCount
rotationAnimation.autoreverses = true
rotationAnimation.fromValue = -angle
rotationAnimation.toValue = angle
rotationAnimation.isRemovedOnCompletion = true
CATransaction.begin()
CATransaction.setCompletionBlock {
if let completion = completion {
completion()
}
}
layer.add(rotationAnimation, forKey: "shakeAnimation")
CATransaction.commit()
}

Resources