How to call a method only after another method completes in ViewDidLoad - swift

I'm trying to make an app where at the start Object A begins to fades.
Immediately after Object A completely fades, I'd like Object B to begin to fade.
I tried putting the below code in the ViewDidLoad method but they both run at the same time. Any idea on how to make one run after the other starting after the initial load?
ObjectA.isHidden = false
UIView.animate(withDuration: 5.0, animations: { () -> Void in
self.Fader.alpha = 0
})
ObjectB.isHidden = false
UIView.animate(withDuration: 1.0, animations: { () -> Void in
self.Intro.alpha = 0
})

You can nest them
UIView.animate(withDuration: 5.0, delay: 0.0, options: [.curveLinear,.allowUserInteraction], animations: {
self.fader.alpha = 0
}) { (fin) in
self.objectB.isHidden = false
UIView.animate(withDuration: 1.0, animations: { () -> Void in
self.Intro.alpha = 0
})
}

Nesting an animation within the first animation's completion block is perfectly fine.
But for grouping multiple animations, it's worthwhile to consider CAAnimation Groups.
here's a ray wenderlich tutorial that goes through a simple CAAnimationGroup
Apple's documentation has a great example, but you MUST specify the beginTime for each animation (otherwise each animation will run at the same time):
let fadeOut = CABasicAnimation(keyPath: "opacity")
fadeOut.fromValue = 1
fadeOut.toValue = 0
fadeOut.duration = 1
fadeOut.beginTime = 0.0
let expandScale = CABasicAnimation()
expandScale.keyPath = "transform"
expandScale.valueFunction = CAValueFunction(name: kCAValueFunctionScale)
expandScale.fromValue = [1, 1, 1]
expandScale.toValue = [3, 3, 3]
expandScale.beginTime = fadeOut.beginTime + fadeOut.duration
let fadeAndScale = CAAnimationGroup()
fadeAndScale.animations = [fadeOut, expandScale]
fadeAndScale.duration = fadeOut.duration + expandScale.duration

Related

Fill the CALayer with different color when timer reaches to 5 seconds swift ios

In my app i am using a third party library which does circular animation just like in the appstore app download animation. i am using the external file which i have placed in my project. it works but when the timer reaches to 5 seconds the fill color should be red. Currently the whole layer red color applies, i want to only apply the portion it has progressed. i am attaching the video and the third party file. Please can anyone help me with this as what changes should i make to the library file or is there better solution
same video link in case google drive link doesnt work
External Library link which i am using
video link
external third file file
My code snippet that i have tried:
func startGamePlayTimer(color: UIColor)
{
if !isCardLiftedUp {
self.circleTimerView.isHidden = false
self.circleTimerView.timerFillColor = color
Constants.totalGamePlayTime = 30
self.circleTimerView.startTimer(duration: CFTimeInterval(Constants.totalGamePlayTime))
}
if self.gamePlayTimer == nil
{
self.gamePlayTimer?.invalidate()
let timer = Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(updateGamePlayTime),
userInfo: nil,
repeats: true)
timer.tolerance = 0.05
RunLoop.current.add(timer, forMode: .common)
self.gamePlayTimer = timer
}
}
#objc func updateGamePlayTime()
{
if Constants.totalGamePlayTime > 0
{
Constants.totalGamePlayTime -= 1
if Constants.totalGamePlayTime < 5
{
SoundService.playSound(sound: .turn_timeout)
self.circleTimerView.timerFillColor = UIColor.red.withAlphaComponent(0.7)
}
}
else
{
self.stopGamePlayTimer()
}
}
where circleTimerView is the view which animates as the time progresses
You can achieve that with CAKeyframeAnimation on the StrokePath.
I've Update the code in CircleTimer View as below. You can modified fill color and time as per your need.
#objc func updateGamePlayTime()
{
if Constants.totalGamePlayTime > 0
{
Constants.totalGamePlayTime -= 1
if Constants.totalGamePlayTime < 5
{
SoundService.playSound(sound: .turn_timeout)
// self.circleTimerView.timerFillColor = UIColor.red.withAlphaComponent(0.7) // <- Comment this line
}
}
else
{
self.stopGamePlayTimer()
}
}
open func drawFilled(duration: CFTimeInterval = 5.0) {
clear()
if filledLayer == nil {
let parentLayer = self.layer
let circleLayer = CAShapeLayer()
circleLayer.bounds = parentLayer.bounds
circleLayer.position = CGPoint(x: parentLayer.bounds.midX, y: parentLayer.bounds.midY)
let circleRadius = timerFillDiameter * 0.5
let circleBounds = CGRect(x: parentLayer.bounds.midX - circleRadius, y: parentLayer.bounds.midY - circleRadius, width: timerFillDiameter, height: timerFillDiameter)
circleLayer.fillColor = timerFillColor.cgColor
circleLayer.path = UIBezierPath(ovalIn: circleBounds).cgPath
// CAKeyframeAnimation: Changing Fill Color on when timer reaches 80% of total time
let strokeAnimation = CAKeyframeAnimation(keyPath: "fillColor")
strokeAnimation.keyTimes = [0, 0.25, 0.75, 0.80]
strokeAnimation.values = [timerFillColor.cgColor, timerFillColor.cgColor, timerFillColor.cgColor, UIColor.red.withAlphaComponent(0.7).cgColor]
strokeAnimation.duration = duration;
circleLayer.add(strokeAnimation, forKey: "fillColor")
parentLayer.addSublayer(circleLayer)
filledLayer = circleLayer
}
}
open func startTimer(duration: CFTimeInterval) {
drawFilled(duration: duration) // <- Need to pass duration here
if useMask {
runMaskAnimation(duration: duration)
} else {
runDrawAnimation(duration: duration)
}
}
UPDATE:
CAKeyframeAnimation:
You specify the keyframe values using the values and keyTimes properties.
For example:
let colorKeyframeAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorKeyframeAnimation.values = [UIColor.red.cgColor,
UIColor.green.cgColor,
UIColor.blue.cgColor]
colorKeyframeAnimation.keyTimes = [0, 0.5, 1]
colorKeyframeAnimation.duration = 2
This animation will run for the 2.0 duration.
We have 3 values and 3 keyTimes in this example.
0 is the initial point and 1 be the last point.
It shows which associated values will reflect at particular interval [keyTimes] during the animation.
i.e:
KeyTime 0.5 -> (2 * 0.5) -> At 1 sec during animation -> UIColor.green.cgColor will be shown.
Now to answer your question from the comments, Suppose we have timer of 25 seconds and we want to show some value for last 10 seconds, we can do:
strokeAnimation.keyTimes = [0, 0.25, 0.50, 0.60]
strokeAnimation.values = [timerFillColor.cgColor,timerFillColor.cgColor, timerFillColor.cgColor, timerFillColor.cgColor, UIColor.red.withAlphaComponent(0.7).cgColor]
strokeAnimation.duration = 25.0 // Let's assume duration is 25.0

Swift UIView zPosition issue, views overlap on other views

I am writing a calendar view, I added three other views to its super view, so the calendar view should be on the topper layer, it looks fine on the simulator
but later on, I found that I can't click the button, then I checked the view hierarchy, found that the other three views overlap on the calendar, it is so weird as the 3d preview and view on the simulator are different
Here is the code of UI Part
func updateUI() {
if calendarView != nil {
self.view.addSubview(calendarView!)
self.calendarView!.frame.origin = calendarViewPosition!
self.calendarView!.layer.zPosition = 5
self.calendarView!.layer.cornerRadius = self.setting.itemCardCornerRadius
self.calendarView!.layer.masksToBounds = true
self.calendarView!.setViewShadow()
UIView.animate(withDuration: 0.8, delay: 0, options: .curveEaseOut, animations: {
self.calendarView!.frame.origin.y += 50
}) { _ in
var newCalendarPageCordiateYDifference: CGFloat = 10
var newCalendarPageSizeDifference: CGFloat = 20
var zPosition: CGFloat = 4
for _ in 1 ... 3 {
let newCalendarPage = UIView()
newCalendarPage.frame = CGRect(x: self.calendarView!.frame.origin.x + newCalendarPageSizeDifference / 2, y: self.calendarView!.frame.origin.y, width: self.calendarView!.frame.width - newCalendarPageSizeDifference, height: self.calendarView!.frame.height - newCalendarPageSizeDifference)
newCalendarPage.backgroundColor = .white
newCalendarPage.layer.zPosition = zPosition
newCalendarPage.layer.cornerRadius = self.setting.itemCardCornerRadius
newCalendarPage.setViewShadow()
self.view.addSubview(newCalendarPage)
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut, animations: {
newCalendarPage.frame.origin.y -= newCalendarPageCordiateYDifference
})
zPosition -= 1
newCalendarPageCordiateYDifference += 10
newCalendarPageSizeDifference += 50
}
}
}
}
Remove self.view.addSubview(newCalendarPage) this from for loop and
Add subview like this
self.view.insertSubview(newCalendarPage, belowSubview: calendarView)
Another way is to disable user interaction like
newCalendarPage.isUserInteractionEnabled = false

Animate UIView with multiple rotations while translating up and down

I have a button called swirlButton that I'd like to have animate a jump up and down, and have it flip 3 times going up and 3 times back down. I'm using translatedBy to move it, and rotated to rotate it, although in this example I only have it animate part of a turn which goes back the other way when the animation finished.
First of all, I have no idea how to make it rotate more than once in the first place. For instance, I can't set the rotated(by:) value to .pi*3, because that seems just to equal 0 degrees and never animate. Same if I set it to .pi.
var transforms: CGAffineTransform = .identity
let jumpDuration:Double = 2.0 //0.8
let halfJumpDuration:Double = jumpDuration/2.0
UIView.animateKeyframes(withDuration: jumpDuration, delay: 0, options: [UIView.KeyframeAnimationOptions.calculationModeCubicPaced], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: halfJumpDuration/jumpDuration, animations: {
transforms = transforms.translatedBy(x: 0, y: -60)
transforms = transforms.rotated(by: .pi/2)
swirlButton.transform = transforms
})
UIView.addKeyframe(withRelativeStartTime: halfJumpDuration/jumpDuration, relativeDuration: halfJumpDuration/jumpDuration, animations: {
transforms = .identity
transforms = transforms.translatedBy(x: 0, y: 0)
swirlButton.transform = transforms
})
},
completion: { _ in
print("animation finished")
})
Aside from going up and down, the rotation is very far from what I would like to happen. Is it difficult to make it spin counterclockwise 3 times going up and continue spinning counterclockwise 3 times going down?
I think it's easier to use CABasicAnimation for this.
Here's what I came up with:
func animateButton() {
swirlButton.layer.add(rotateAnimation(), forKey: nil)
CATransaction.begin()
let upAnimation = bounceAnimation()
CATransaction.setCompletionBlock{ () in
self.swirlButton.layer.add(self.bounceAnimation(animatingDown: true), forKey: nil)
}
swirlButton.layer.add(upAnimation, forKey: nil)
CATransaction.commit()
}
func rotateAnimation() -> CABasicAnimation {
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = 0
rotate.toValue = -6*CGFloat.pi
rotate.duration = 2
return rotate
}
func bounceAnimation(animatingDown: Bool = false) -> CABasicAnimation {
let buttonY = swirlButton.layer.position.y
let buttonX = swirlButton.layer.position.x
let translate = CABasicAnimation(keyPath: "position")
translate.fromValue = animatingDown ? [buttonX, buttonY - 200] : [buttonX, buttonY]
translate.toValue = animatingDown ? [buttonX, buttonY] : [buttonX, buttonY - 200]
translate.duration = 1
translate.fillMode = .forwards
translate.isRemovedOnCompletion = false
return translate
}
translate.fillMode = .forwards & translate.isRemovedOnCompletion = false is necesarry to prevent flicker in-between the animations and CATransaction allows us to set a completion block for when the first animation (up) finishes.

UIView.animateKeyFramesWithDuration not animating smoothly

I am trying to write a blinking animation, but the timing of it feels off. I made a simple version in a playground:
import UIKit
import XCPlayground
let v = UIView()
v.frame.size = CGSize(width: 200, height: 200)
v.backgroundColor = UIColor.redColor()
v.layer.cornerRadius = 100
UIView.animateKeyframesWithDuration(1, delay: 0, options: .Repeat, animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: {
v.alpha = 0.0
})
UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.5, animations: {
v.alpha = 0.5
})
}, completion: nil)
XCPlaygroundPage.currentPage.liveView = v
The view fades from 0 to 0.5 and then appears to stutter at full alpha for a second before resuming the animation. I noticed the same thing in the simulator.
Is there something I am missing about how the keyframes are supposed to work?
Upon inspection, I believe the default for your v view's alpha is 1.0. This means that after your animation ends for a split second it is at full alpha again, and then the animation is repeated. To compensate for this and acquire the effect you want, you may consider setting its alpha to 0.0 before you run XCPlaygroundPage.currentPage.liveView = v.
Updates:
You can break the animation your code into 4 states.
At the start, the alpha is 1.0.
The first keyframe changes the alpha to 0.0 in 0.5 seconds.
The second keyframe changes the alpha to 0.5 in 0.5 seconds.
At this point, the animation has ended. So the v view reverts to state 1 and repeats then animation.
State 4 is where the blink of full alpha occurs because the v view is going from 0.5 to 1.0 in 0.0 seconds. However, the computer cannot make anything happen in 0.0 seconds (not actually possible because of complicated physics) so the result is a split second flash of full alpha as the computer tries to get as close to 0.0 seconds as it can.
To circumvent this you can either set the original alpha to 0.5 so that way the animation's state 1 will be the same as the result of its state 3, or you can add another keyframe that brings the alpha back to 0.0 before the animation is over:
Examples:
Option 1:
//Import Statements
//Configuration and Initialization of v view
v.alpha = 0.5 //<--- This is the key point
UIView.animateKeyframesWithDuration(1, delay: 0, options: .Repeat, animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0.5, animations: {
v.alpha = 0.0
})
UIView.addKeyframeWithRelativeStartTime(0.5, relativeDuration: 0.5, animations: {
v.alpha = 0.5
})
}, completion: nil)
XCPlaygroundPage.currentPage.liveView = v
Option 2:
//Import Statements
//Configuration and Initialization of v view
v.alpha = 0.0 //<-- Make sure to set the original alpha to 0.0
let duration: NSTimeInterval = 1.8
let third: NSTimeInterval = 1/3
UIView.animateKeyframesWithDuration(duration, delay: 0, options: .Repeat, animations: {
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: third, animations: {
v.alpha = 0.0
})
UIView.addKeyframeWithRelativeStartTime(third, relativeDuration: third, animations: {
v.alpha = 0.5
})
//Key Point Below. Added Another Frame!
UIView.addKeyframeWithRelativeStartTime(third*2, relativeDuration: third, animations: {
v.alpha = 0.0
})
}, completion: nil)
XCPlaygroundPage.currentPage.liveView = v

UIButton Heartbeat Animation

I've created a heart beat animation for a UIButton. However, there is no way to stop this animation as it's an endless code loop. After tinkering with numerous UIView animation code blocks I've been unable to get UIViewAnimationOptions.Repeat to produce what I need. If I could do that I could simply button.layer.removeAllAnimations() to remove the animations. What is a way to write this that allows for removal of the animation? I'm thinking a timer possibly but that could be kind of messy with multiple animations going on.
func heartBeatAnimation(button: UIButton) {
button.userInteractionEnabled = true
button.enabled = true
func animation1() {
UIView.animateWithDuration(0.5, delay: 0.0, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}, completion: nil)
UIView.animateWithDuration(0.5, delay: 0.5, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}) { (Bool) -> Void in
delay(2.0, closure: { () -> () in
animation2()
})
}
}
func animation2() {
UIView.animateWithDuration(0.5, delay: 0.0, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}, completion: nil)
UIView.animateWithDuration(0.5, delay: 0.5, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}) { (Bool) -> Void in
delay(2.0, closure: { () -> () in
animation1()
})
}
}
animation1()
}
This works perfectly. The damping and spring need to be tweaked a little bit but this solves the problem. removeAllAnimations() clears the animation and returns the button to it's normal state.
button.userInteractionEnabled = true
button.enabled = true
let pulse1 = CASpringAnimation(keyPath: "transform.scale")
pulse1.duration = 0.6
pulse1.fromValue = 1.0
pulse1.toValue = 1.12
pulse1.autoreverses = true
pulse1.repeatCount = 1
pulse1.initialVelocity = 0.5
pulse1.damping = 0.8
let animationGroup = CAAnimationGroup()
animationGroup.duration = 2.7
animationGroup.repeatCount = 1000
animationGroup.animations = [pulse1]
button.layer.addAnimation(animationGroup, forKey: "pulse")
This post was very helpful: CAKeyframeAnimation delay before repeating
Swift 5 code, works without the pause between pulses:
let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.duration = 0.4
pulse.fromValue = 1.0
pulse.toValue = 1.12
pulse.autoreverses = true
pulse.repeatCount = .infinity
pulse.initialVelocity = 0.5
pulse.damping = 0.8
switchButton.layer.add(pulse, forKey: nil)
I created something like this:
let animation = CAKeyframeAnimation(keyPath: "transform.scale")
animation.values = [1.0, 1.2, 1.0]
animation.keyTimes = [0, 0.5, 1]
animation.duration = 1.0
animation.repeatCount = Float.infinity
layer.add(animation, forKey: "pulse")
On your original question you mentioned that you want the animation to stop on command. I assume you would like it to start on command too. This solution will do both and it is quite simple.
func cutAnim(){
for view in animating {
///I use a UIView because I wanted the container of my button to be animated. UIButton will work just fine too.
(view.value as? UIView)?.layer.removeAllAnimations()
}
}
func pulse(button: UIButton, name: String){
///Here I capture that container
let container = button.superview?.superview
///Add to Dictionary
animating[name] = container
cutAnim()
UIView.animate(withDuration: 1, delay: 0.0, options:[UIViewAnimationOptions.repeat, UIViewAnimationOptions.autoreverse, .allowUserInteraction], animations: {
container?.transform = CGAffineTransform(scaleX: 1.15, y: 1.15)
///if you stop the animation half way it completes anyways so I want the container to go back to its original size
container?.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}, completion: nil)
}
Call cutAnim() anywhere to stop an animation, inside a timer if you want.
To start the animation use a regular button action
#IBAction func buttonWasTappedAction(_ sender: Any) {
pulse(button: sender as! UIButton, name: "nameForDictionary")
}
Hope this helps.