Hi I want to make animation with 3 UIView. The main problem is I can't start animation once it's stopped by removing animation from layer.
Here is the code:
class HTAnimatedTypingView: UIView {
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
#IBOutlet weak var view3: UIView!
func startAnimation(){
UIView.animate(withDuration: 0.5, delay: 0, options: .repeat, animations: {
self.view1.frame.origin.y = 0
}, completion: nil)
UIView.animate(withDuration: 0.3, delay: 0.5, options: .repeat, animations: {
self.view2.frame.origin.y = 0
}, completion: nil)
UIView.animate(withDuration: 0.2, delay: 1.0, options: .repeat, animations: {
self.view3.frame.origin.y = 0
}, completion: nil)
}
func stopAnimations(){
self.view1.layer.removeAllAnimations()
self.view2.layer.removeAllAnimations()
self.view3.layer.removeAllAnimations()
}
}
Output of Above Code:
Expected Animation:
How can make it work with start animation & stop animation functionality? Thanks in advance...
Since you need to add some pause in between each sequence of animations, I would personally do it using key frames as it gives you some flexibility:
class AnimationViewController: UIViewController {
private let stackView: UIStackView = {
$0.distribution = .fill
$0.axis = .horizontal
$0.alignment = .center
$0.spacing = 10
return $0
}(UIStackView())
private let circleA = UIView()
private let circleB = UIView()
private let circleC = UIView()
private lazy var circles = [circleA, circleB, circleC]
func animate() {
let jumpDuration: Double = 0.30
let delayDuration: Double = 1.25
let totalDuration: Double = delayDuration + jumpDuration*2
let jumpRelativeDuration: Double = jumpDuration / totalDuration
let jumpRelativeTime: Double = delayDuration / totalDuration
let fallRelativeTime: Double = (delayDuration + jumpDuration) / totalDuration
for (index, circle) in circles.enumerated() {
let delay = jumpDuration*2 * TimeInterval(index) / TimeInterval(circles.count)
UIView.animateKeyframes(withDuration: totalDuration, delay: delay, options: [.repeat], animations: {
UIView.addKeyframe(withRelativeStartTime: jumpRelativeTime, relativeDuration: jumpRelativeDuration) {
circle.frame.origin.y -= 30
}
UIView.addKeyframe(withRelativeStartTime: fallRelativeTime, relativeDuration: jumpRelativeDuration) {
circle.frame.origin.y += 30
}
})
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
circles.forEach {
$0.layer.cornerRadius = 20/2
$0.layer.masksToBounds = true
$0.backgroundColor = .systemBlue
stackView.addArrangedSubview($0)
$0.widthAnchor.constraint(equalToConstant: 20).isActive = true
$0.heightAnchor.constraint(equalTo: $0.widthAnchor).isActive = true
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animate()
}
}
It should be pretty straightforward, but feel free to let me know if you have any questions!
And this is how the result looks like:
One way could be to use a Timer. Keep an instance of Timer in your class. When startAnimation is called, schedule it. When stopAnimation is called, invalidate it. (This means that the currently ongoing animation will be completed before the animation actually stops, which IMO makes it a nice non-abrupt stop).
On each tick of the timer, animate the dots once. Note that the animation you apply on each dot should have the same duration, as in the expected output, they all bounce at the same rate, just at different instants in time.
Some illustrative code:
// startAnimation
timer = Timer.scheduledTimer(withTimeInterval: timerInterval, repeats: true) { _ in
self.animateDotsOnce()
}
// stopAnimation
timer.invalidate()
// animateDotsOnce
UIView.animate(withDuration: animationDuration, delay: 0, animations: {
self.view1.frame.origin.y = animateHeight
}, completion: {
_ in
UIView.animate(withDuration: animationDuration) {
self.view1.frame.origin.y = 0
}
})
// plus the other two views, with different delays...
I'll leave it to you to find a suitable animateHeight, timerInterval, animationDuration and delays for each view.
I'd recommend using a CAKeyframeAnimation instead of handling completion blocks and that sorcery. Here's a quick example:
for i in 0 ..< 3 {
let bubble = UIView(frame: CGRect(x: 20 + i * 20, y: 200, width: 10, height: 10))
bubble.backgroundColor = .red
bubble.layer.cornerRadius = 5
self.view.addSubview(bubble)
let animation = CAKeyframeAnimation()
animation.keyPath = "position.y"
animation.values = [0, 10, 0]
animation.keyTimes = [0, 0.5, 1]
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.duration = 1
animation.isAdditive = true
animation.repeatCount = HUGE
animation.timeOffset = CACurrentMediaTime() + 0.2 * Double(i)
bubble.layer.add(animation, forKey: "anim")
}
When you wanna remove the animation you just use bubble.layer.removeAnimation(forKey: "anim"). You might have to play around with the timing function or values and keyTimes to get the exact movement you want. But keyframes is the way to go to make a specific animation.
Side note: this example won't work in viewDidLoad cause the view doesn't have a superview yet so the animation won't work. If you test it in viewDidAppear it will work.
UIView animation is different to CALayer animation,best not to mix them.
Write locally and tested.
import UIKit
import SnapKit
class HTAnimatedTypingView: UIView {
private let view0 = UIView()
private let view1 = UIView()
private let view2 = UIView()
init() {
super.init(frame: CGRect.zero)
makeUI()
}
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
makeUI()
}
private func makeUI() {
backgroundColor = UIColor.white
view0.backgroundColor = UIColor.red
view1.backgroundColor = UIColor.blue
view2.backgroundColor = UIColor.yellow
addSubview(view0)
addSubview(view1)
addSubview(view2)
view0.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.left.equalTo(self.snp.left)
}
view1.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.centerX.equalTo(self.snp.centerX)
}
view2.snp.makeConstraints { (make) in
make.centerY.equalTo(self.snp.centerY)
make.width.equalTo(10)
make.height.equalTo(10)
make.right.equalTo(self.snp.right)
}
}
public func startAnimation() {
let duration:CFTimeInterval = 0.5
let animation_delay:CFTimeInterval = 0.1
assert(duration >= animation_delay * 5, "animation_delay should be way smaller than duration in order to make animation natural")
let translateAnimation = CABasicAnimation(keyPath: "position.y")
translateAnimation.duration = duration
translateAnimation.repeatCount = Float.infinity
translateAnimation.toValue = 0
translateAnimation.fillMode = CAMediaTimingFillMode.both
translateAnimation.isRemovedOnCompletion = false
translateAnimation.autoreverses = true
view0.layer.add(translateAnimation, forKey: "translation")
DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay) { [unowned self ] in
self.view1.layer.add(translateAnimation, forKey: "translation")
}
DispatchQueue.main.asyncAfter(deadline: .now() + animation_delay * 2) { [unowned self ] in
self.view2.layer.add(translateAnimation, forKey: "translation")
}
}
public func stopAnimation() {
self.view0.layer.removeAllAnimations()
self.view1.layer.removeAllAnimations()
self.view2.layer.removeAllAnimations()
}
}
Related
I trying to create a spin wheel which rotate on tap and it rotates for certain period of time and stops at some random circular angle.
import UIKit
class MasterViewController: UIViewController {
lazy var imageView: UIImageView = {
let bounds = self.view.bounds
let v = UIImageView()
v.backgroundColor = .red
v.frame = CGRect(x: 0, y: 0,
width: bounds.width - 100,
height: bounds.width - 100)
v.center = self.view.center
return v
}()
lazy var subView: UIView = {
let v = UIView()
v.backgroundColor = .black
v.frame = CGRect(x: 0, y: 0,
width: 30,
height: 30)
return v
}()
var dateTouchesEnded: Date?
var dateTouchesStarted: Date?
var deltaAngle = CGFloat(0)
var startTransform: CGAffineTransform?
var touchPointStart: CGPoint?
override func viewDidLoad() {
super.viewDidLoad()
self.imageView.addSubview(self.subView)
self.view.addSubview(self.imageView)
self.imageView.isUserInteractionEnabled = true
self.setupGesture()
}
private func setupGesture() {
let gesture = UITapGestureRecognizer(target: self,
action: #selector(handleGesture(_:)))
self.view.addGestureRecognizer(gesture)
}
#objc
func handleGesture(_ sender: UITapGestureRecognizer) {
var timeDelta = 1.0
let _ = Timer.scheduledTimer(withTimeInterval: 0.2,
repeats: true) { (timer) in
if timeDelta < 0 {
timer.invalidate()
} else {
timeDelta -= 0.03
self.spinImage(timeDelta: timeDelta)
}
}
}
func spinImage(timeDelta: Double) {
print("TIME DELTA:", timeDelta)
let direction: Double = 1
let rotation: Double = 1
UIView.animate(withDuration: 5,
delay: 0,
options: .curveEaseOut,
animations: {
let transform = self.imageView.transform.rotated(
by: CGFloat(direction) * CGFloat(rotation) * CGFloat(Double.pi)
)
self.imageView.transform = transform
}, completion: nil)
}
}
I tried via above code, but always stops on initial position
i.e. the initial and final transform is same.
I want it to be random at every time.
One thing you can do is make a counter with a random number within a specified range. When the time fires, call spinImage then decrement the counter. Keep doing that until the counter reaches zero. This random number will give you some variability so that you don't wind up with the same result every time.
#objc func handleGesture(_ sender: UITapGestureRecognizer) {
var counter = Int.random(in: 30...33)
let _ = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { (timer) in
if counter < 0 {
timer.invalidate()
} else {
counter -= 1
self.spinImage()
}
}
}
In spinImage, instead of rotating by CGFloat.pi, rotate by by CGFloat.pi / 2 so that you have four possible outcomes instead of two.
func spinImage() {
UIView.animate(withDuration: 2.5,
delay: 0,
options: .curveEaseOut,
animations: {
let transform = self.imageView.transform.rotated(
by: CGFloat.pi / 2
)
self.imageView.transform = transform
}, completion: nil)
}
You may want to mess around with the counter values, the timer interval, and the animation duration to get the effect that you want. The values I chose here are somewhat arbitrary.
I have view to display small indicator bars (for sound)
and here it is code for this:
class IndicatorView: UIViewController {
enum AudioState {
case stop
case play
case pause
}
var state: AudioState! {
didSet {
switch state {
case .pause:
pauseAnimation()
case .play:
playAnimation()
default:
stopAnimation()
}
}
}
var numberOfBars: Int = 5
var barWidth: CGFloat = 4
var barSpacer: CGFloat = 4
var barColor: UIColor = .systemPink
private var bars: [UIView] = [UIView]()
private func stopAnimation() {
bars.forEach { $0.alpha = 0 }
}
private func pauseAnimation() {
bars.forEach {
$0.layer.speed = 0
$0.transform = CGAffineTransform(scaleX: 1, y: 0.1)
}
}
private func playAnimation() {
bars.forEach {
$0.alpha = 1
$0.layer.speed = 1
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
DispatchQueue.main.async {
self.setupViews()
}
}
private func setupViews() {
for i in 0...numberOfBars - 1 {
let b = UIView()
b.backgroundColor = barColor
addAnimation(to: b)
view.addSubview(b)
bars.append(b)
b.anchor(top: view.topAnchor, leading: view.leadingAnchor, bottom: view.bottomAnchor, trailing: nil,
padding: .init(top: 0, left: CGFloat(i) * (barWidth + barSpacer), bottom: 0, right: 0),
size: .init(width: barWidth, height: 0))
}
stopAnimation()
}
private func addAnimation(to v: UIView) {
let animation = CAKeyframeAnimation()
animation.keyPath = "transform.scale.y"
animation.values = [0.1, 0.3, 0.2, 0.5, 0.8, 0.3, 0.99, 0.72, 0.3].shuffled()
animation.duration = 1
animation.autoreverses = true
animation.repeatCount = .infinity
v.layer.add(animation, forKey: "baran")
}
}
and work fine. I'm using it from another vc..etc.
Problem
When app moved to background, music player paused, and in IndicatorView state = .pause is assigned, but when app come back, and user tap on play eg. in IndicatorView state = .play
playAnimation() is called, bars layers have speed one... but no animation at all.
Here is a short video to describe my problem
Thanks
When the app goes to background the CALayer animation is paused. You could implement methods to pause and resume animation when going to bg/fg, but for your case if you move the call to "addAnimation(to: b)" from the setupviews to the "playAnimation()" method, you can guarantee the animation will always be there. ex:
bars.forEach {
$0.alpha = 1
addAnimation(to: $0)
$0.layer.speed = 1
}
Hope it helps :)
I am trying to perform a transition animation whenever a user scrolls on a paginated view, i.e: From Page 1 to Page 2.
Unfortunately, I've not been able to replicate that.
Attach below is what I've done:
class OnboardingParallaxImageView: BaseUIView, UIScrollViewDelegate {
let allImages = [#imageLiteral(resourceName: "onboarding_handshake_icon"), #imageLiteral(resourceName: "onboarding_paylinc_icon")]
var activeCurrentPage = 1
let bgView: UIImageView = {
let image = #imageLiteral(resourceName: "onboard_bg_gradient")
let view = UIImageView(image: image)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let firstImageView: UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let secondImageView: UIImageView = {
let view = UIImageView()
view.isHidden = true
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var firstImageHeightAnchor: NSLayoutConstraint?
var firstImageWidthAnchor: NSLayoutConstraint?
var secondImageHeightAnchor: NSLayoutConstraint?
var secondImageWidthAnchor: NSLayoutConstraint?
override func setupViews() {
super.setupViews()
addSubview(bgView)
addSubview(firstImageView)
addSubview(secondImageView)
bgView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
bgView.heightAnchor.constraint(equalTo: heightAnchor).isActive = true
bgView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
bgView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
firstImageWidthAnchor = firstImageView.widthAnchor.constraint(equalTo: widthAnchor)
firstImageWidthAnchor?.isActive = true
firstImageHeightAnchor = firstImageView.heightAnchor.constraint(equalTo: heightAnchor)
firstImageHeightAnchor?.isActive = true
firstImageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
firstImageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
firstImageView.image = allImages[0]
secondImageWidthAnchor = secondImageView.widthAnchor.constraint(equalTo: widthAnchor)
secondImageWidthAnchor?.isActive = true
secondImageHeightAnchor = secondImageView.heightAnchor.constraint(equalTo: heightAnchor)
secondImageHeightAnchor?.isActive = true
secondImageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
secondImageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
secondImageView.image = allImages[1]
}
override func layoutSubviews() {
super.layoutSubviews()
let frameWidth = frame.size.width
secondImageHeightAnchor?.constant = -(frameWidth - 32)
secondImageWidthAnchor?.constant = -(frameWidth - 32)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offSet = scrollView.contentOffset.x
let frameWidth = frame.size.width / 2
let toUseConstant = (CGFloat(abs(offSet)) / frameWidth)
if activeCurrentPage == 1 {
if offSet <= 0 {
firstImageHeightAnchor?.constant = 0
firstImageWidthAnchor?.constant = 0
firstImageView.isHidden = false
secondImageView.isHidden = true
} else {
firstImageHeightAnchor?.constant += -(toUseConstant)
firstImageWidthAnchor?.constant += -(toUseConstant)
firstImageView.isHidden = false
secondImageHeightAnchor?.constant += -(toUseConstant)
secondImageWidthAnchor?.constant += -(toUseConstant)
secondImageView.isHidden = false
secondImageView.alpha = toUseConstant
}
}
UIView.animate(withDuration: 0.5) {
self.layoutSubviews()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.activeCurrentPage = scrollView.currentPage
}
}
This is the result of what I've been able to achieve:
How can I go about transitioning from A to B without any funny behaviour.
Thanks
The easiest way to achieve smooth behaviour is using UIViewPropertyAnimator.
Setup the initial values for each image view:
override func viewDidLoad() {
super.viewDidLoad()
firstImageView.image = // your image
secondImageView.image = // your image
secondImageView.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
secondImageView.alpha = 0
}
Then create property animator for each imageView
lazy var firstAnimator: UIViewPropertyAnimator = {
// You can play around with the duration, curve, damping ration, timing
let animator = UIViewPropertyAnimator(duration: 2, curve: .easeIn, animations: {
self.firstImageView.image.transform = CGAffineTransform(scaleX: 0.01, y: 0.01)
self.firstImageView.image.alpha = 0
})
return animator
}()
and the second one
lazy var secondAnimator: UIViewPropertyAnimator = {
let animator = UIViewPropertyAnimator(duration: 2, curve: .easeIn, animations: {
self.secondImageView.transform = CGAffineTransform(scaleX: 1, y: 1)
self.secondImageView.alpha = 1
})
return animator
}()
now on scrollViewDidScroll when you have calculated the completed percent just update the animators:
firstAnimator.fractionComplete = calculatedPosition
secondAnimator.fractionComplete = calculatedPosition
You can apply the same approach for multiple views
You might want to consider using a Framework for this instead of reinventing the wheel. Something like Hero for example.
There as some basic examples to get you started. Essentially this provides you an easy to use option to code transition behavior between two UIViewControllers by defining how each UI-Component should behave base on the transition progress.
I have achieved it using the keyframe animation on ImageView's frame. But you can also achieve it using the constraint based animation as you tried. Just replace the frames size variations that I have done here with the change in constraint's constants as per your convenience.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var imgView1: UIImageView! //HANDSHAKE image on TOP
#IBOutlet weak var imgView2: UIImageView! //CREDIT_CARD and PHONE image
#IBOutlet weak var imgView3: UIImageView! //SINGLE_HAND image
let minimumFrameSize = CGSize(width: 0.0, height: 0.0)
let meetingPointFrameSize = CGSize(width: 150.0, height: 150.0)
let maximumFrameSize = CGSize(width: 400.0, height: 400.0)
override func viewDidLoad() {
super.viewDidLoad()
let centerOfImages = CGPoint(x: self.view.frame.width/2 , y: self.view.frame.height/2)
//initial state
self.imgView1.frame.size = maximumFrameSize
self.imgView1.alpha = 1.0
self.imgView2.frame.size = minimumFrameSize
self.imgView2.alpha = 0.0
self.imgView3.frame.size = minimumFrameSize
self.imgView3.alpha = 0.0
self.imgView1.center = centerOfImages
self.imgView2.center = centerOfImages
self.imgView3.center = centerOfImages
UIView.animateKeyframes(withDuration: 5.0, delay: 2.0, options: [UIView.KeyframeAnimationOptions.repeat,
UIView.KeyframeAnimationOptions.calculationModeLinear],
animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations: {
self.imgView1.frame.size = self.meetingPointFrameSize
self.imgView1.alpha = 0.3
self.imgView1.center = centerOfImages
self.imgView2.frame.size = self.meetingPointFrameSize
self.imgView2.alpha = 0.3
self.imgView2.center = centerOfImages
/*image 1 and image 2 meets at certain point where
image 1 is decreasing its size and image 2 is increasing its size simultaneously
*/
})
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25, animations: {
self.imgView1.frame.size = self.minimumFrameSize
self.imgView1.alpha = 0.0
self.imgView1.center = centerOfImages
self.imgView2.frame.size = self.maximumFrameSize
self.imgView2.alpha = 1.0
self.imgView2.center = centerOfImages
/* image 1 has decreased its size to zero and
image 2 has increased its size to maximum simultaneously
*/
})
UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.1, animations: {
self.imgView2.frame.size = self.maximumFrameSize
self.imgView2.alpha = 1.0
self.imgView2.center = centerOfImages
/* Hold for a moment
*/
})
UIView.addKeyframe(withRelativeStartTime: 0.6, relativeDuration: 0.15, animations: {
self.imgView2.frame.size = self.meetingPointFrameSize
self.imgView2.alpha = 0.3
self.imgView2.center = centerOfImages
self.imgView3.frame.size = self.meetingPointFrameSize
self.imgView3.alpha = 0.3
self.imgView3.center = centerOfImages
/*image 2 and image 3 meets at certain point where
image 2 is decreasing its size and image 3 is increasing its size simultaneously
*/
})
UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.25, animations: {
self.imgView2.frame.size = self.minimumFrameSize
self.imgView2.alpha = 0.0
self.imgView2.center = centerOfImages
self.imgView3.frame.size = self.maximumFrameSize
self.imgView3.alpha = 1.0
self.imgView3.center = centerOfImages
/* image 2 has decreased its size to zero and
image 3 has increased its size to maximum simultaneously
*/
})
}) { (finished) in
/*If you have any doubt or need more enhancement or in case you need my project. Feel free to ask me.
Please ping me on skype/email:
ojhashubham29#gmail.com
or connect me on Twitter
#hearthackman
*/
}
}
}
My animation stopped running when I press the home button and then relaunch the app. The settings button just stop spinning and the blink label just faded away. Here is my code for both animation:
Blink animation:
extension UILabel {
func startBlink() {
UIView.animate(withDuration: 0.8,
delay:0.0,
options:[.autoreverse, .repeat],
animations: {
self.alpha = 0
}, completion: nil)
}
}
Rotating animation:
extension UIButton {
func startRotating() {
UIView.animate(withDuration: 4.0, delay: 0.0, options:[.autoreverse, .repeat,UIViewAnimationOptions.allowUserInteraction], animations: {
self.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
}, completion: nil)
}
}
Where I run it:
override func viewDidLoad() {
super.viewDidLoad()
settingsButton.layer.cornerRadius = 0.5 * settingsButton.bounds.size.width
settingsButton.clipsToBounds = true
settingsButton.imageView?.contentMode = .scaleAspectFit
NotificationCenter.default.addObserver(self, selector: #selector(appMovedToForeground), name: Notification.Name.UIApplicationWillEnterForeground, object: nil)
}
func appMovedToForeground() {
tapToPlayLabel.startBlink()
settingsButton.startRotating()
print("DID")
}
To restart your animation you have to do below thing, please check below code.
Check extension
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tapToPlayLabel: UILabel!
#IBOutlet weak var settingsButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
settingsButton.layer.cornerRadius = settingsButton.frame.size.width/2
settingsButton.clipsToBounds = true
//settingsButton.imageView?.contentMode = .scaleAspectFit
settingsButton.startRotating()
tapToPlayLabel.startBlink()
NotificationCenter.default.addObserver(self, selector: #selector(appMovedToForeground), name: Notification.Name.UIApplicationWillEnterForeground, object: nil)
}
func appMovedToForeground() {
self.tapToPlayLabel.startBlink()
self.settingsButton.startRotating()
}
}
extension UILabel {
func startBlink() {
self.alpha = 1
UIView.animate(withDuration: 0.8,
delay:0.0,
options:[.autoreverse, .repeat],
animations: {
self.alpha = 0
}, completion: nil)
}
}
extension UIButton {
func startRotating() {
self.transform = CGAffineTransform(rotationAngle: CGFloat.pi/2)
UIView.animate(withDuration: 4.0, delay: 0.0, options:[.autoreverse, .repeat,UIViewAnimationOptions.allowUserInteraction], animations: {
self.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
}, completion: nil)
}
}
Output
I think you need to run your animations with a little delay as there is already the delegate of app in execution while app is moving to foreground.
Or you can add CALayerAnimation on UILabel
extension UILabel {
func startBlink() {
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform")
scaleAnimation.delegate = self as? CAAnimationDelegate
let transform: CATransform3D = CATransform3DMakeScale(1.5, 1.5, 1)
scaleAnimation.values = [NSValue(caTransform3D: CATransform3DIdentity), NSValue(caTransform3D: transform), NSValue(caTransform3D: CATransform3DIdentity)]
scaleAnimation.duration = 0.5
scaleAnimation.repeatCount = 100000000
self.layer.add(scaleAnimation as? CAAnimation ?? CAAnimation(), forKey: "scaleText")
}
func startRotating() {
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = NSNumber(value: Double.pi * 2)
rotation.duration = 2
rotation.isCumulative = true
rotation.repeatCount = .greatestFiniteMagnitude
self.layer.add(rotation, forKey: "rotationAnimation")
}
}
I am trying to make a simple animation in Swift. It is a fade in.
I attempted:
self.myFirstLabel.alpha = 0
self.myFirstButton.alpha = 0
self.mySecondButton.alpha = 0
Then, I have:
self.view.addSubview(myFirstLabel)
self.view.addSubview(myFirstButton)
self.view.addSubview(mySecondButton)
And then:
UIView.animateWithDuration(1.5, animations: {
self.myFirstLabel.alpha = 1.0
self.myFirstButton.alpha = 1.0
self.mySecondButton.alpha = 1.0
})
I have all of this in my viewDidLoad function.
How do I make this work?
The problem is that you're trying start the animation too early in the view controller's lifecycle. In viewDidLoad, the view has just been created, and hasn't yet been added to the view hierarchy, so attempting to animate one of its subviews at this point produces bad results.
What you really should be doing is continuing to set the alpha of the view in viewDidLoad (or where you create your views), and then waiting for the viewDidAppear: method to be called. At this point, you can start your animations without any issue.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 1.5) {
self.myFirstLabel.alpha = 1.0
self.myFirstButton.alpha = 1.0
self.mySecondButton.alpha = 1.0
}
}
0x7ffffff's answer is ok and definitely exhaustive.
As a plus, I suggest you to make an UIView extension, in this way:
public extension UIView {
/**
Fade in a view with a duration
- parameter duration: custom animation duration
*/
func fadeIn(duration duration: NSTimeInterval = 1.0) {
UIView.animateWithDuration(duration, animations: {
self.alpha = 1.0
})
}
/**
Fade out a view with a duration
- parameter duration: custom animation duration
*/
func fadeOut(duration duration: NSTimeInterval = 1.0) {
UIView.animateWithDuration(duration, animations: {
self.alpha = 0.0
})
}
}
Swift-3
/// Fade in a view with a duration
///
/// Parameter duration: custom animation duration
func fadeIn(withDuration duration: TimeInterval = 1.0) {
UIView.animate(withDuration: duration, animations: {
self.alpha = 1.0
})
}
/// Fade out a view with a duration
///
/// - Parameter duration: custom animation duration
func fadeOut(withDuration duration: TimeInterval = 1.0) {
UIView.animate(withDuration: duration, animations: {
self.alpha = 0.0
})
}
Swift-5
public extension UIView {
/**
Fade in a view with a duration
- parameter duration: custom animation duration
*/
func fadeIn(duration: TimeInterval = 1.0) {
UIView.animate(withDuration: duration, animations: {
self.alpha = 1.0
})
}
/**
Fade out a view with a duration
- parameter duration: custom animation duration
*/
func fadeOut(duration: TimeInterval = 1.0) {
UIView.animate(withDuration: duration, animations: {
self.alpha = 0.0
})
}
}
In this way you can do this wherever in your code:
let newImage = UIImage(named: "")
newImage.alpha = 0 // or newImage.fadeOut(duration: 0.0)
self.view.addSubview(newImage)
...
newImage.fadeIn()
Code reuse is important!
Swift only solution
Similar to Luca's anwer, I use a UIView extension. Compared to his solution I use DispatchQueue.main.async to make sure animations are done on the main thread, alpha parameter for fading to a specific value and optional duration parameters for cleaner code.
extension UIView {
func fadeTo(_ alpha: CGFloat, duration: TimeInterval = 0.3) {
DispatchQueue.main.async {
UIView.animate(withDuration: duration) {
self.alpha = alpha
}
}
}
func fadeIn(_ duration: TimeInterval = 0.3) {
fadeTo(1.0, duration: duration)
}
func fadeOut(_ duration: TimeInterval = 0.3) {
fadeTo(0.0, duration: duration)
}
}
How to use it:
// fadeIn() - always animates to alpha = 1.0
yourView.fadeIn() // uses default duration of 0.3
yourView.fadeIn(1.0) // uses custom duration (1.0 in this example)
// fadeOut() - always animates to alpha = 0.0
yourView.fadeOut() // uses default duration of 0.3
yourView.fadeOut(1.0) // uses custom duration (1.0 in this example)
// fadeTo() - used if you want a custom alpha value
yourView.fadeTo(0.5) // uses default duration of 0.3
yourView.fadeTo(0.5, duration: 1.0)
If you want repeatable fade animation you can do that by using CABasicAnimation like below :
First create handy UIView extension :
extension UIView {
enum AnimationKeyPath: String {
case opacity = "opacity"
}
func flash(animation: AnimationKeyPath ,withDuration duration: TimeInterval = 0.5, repeatCount: Float = 5){
let flash = CABasicAnimation(keyPath: animation.rawValue)
flash.duration = duration
flash.fromValue = 1 // alpha
flash.toValue = 0 // alpha
flash.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
flash.autoreverses = true
flash.repeatCount = repeatCount
layer.add(flash, forKey: nil)
}
}
How to use it:
// You can use it with all kind of UIViews e.g. UIButton, UILabel, UIImage, UIImageView, ...
imageView.flash(animation: .opacity, withDuration: 1, repeatCount: 5)
titleLabel.flash(animation: .opacity, withDuration: 1, repeatCount: 5)
Swift 5
Other answers are correct, but in my case I need to handle other properties also (alpha, animate, completion). Because of this, I modified a bit to expose these parameters as below:
extension UIView {
/// Helper function to update view's alpha with animation
/// - Parameter alpha: View's alpha
/// - Parameter animate: Indicate alpha changing with animation or not
/// - Parameter duration: Indicate time for animation
/// - Parameter completion: Completion block after alpha changing is finished
func set(alpha: CGFloat, animate: Bool, duration: TimeInterval = 0.3, completion: ((Bool) -> Void)? = nil) {
let animation = { (view: UIView) in
view.alpha = alpha
}
if animate {
UIView.animate(withDuration: duration, animations: {
animation(self)
}, completion: { finished in
completion?(finished)
})
} else {
layer.removeAllAnimations()
animation(self)
completion?(true)
}
}
}
import UIKit
/*
Here is simple subclass for CAAnimation which create a fadeIn animation
*/
class FadeInAdnimation: CABasicAnimation {
override init() {
super.init()
keyPath = "opacity"
duration = 2.0
fromValue = 0
toValue = 1
fillMode = CAMediaTimingFillMode.forwards
isRemovedOnCompletion = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
/*
Example of usage
*/
class ViewController: UIViewController {
weak var label: UILabel!
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.alpha = 0
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black
view.addSubview(label)
self.label = label
let button = UIButton(type: .custom)
button.frame = CGRect(x: 0, y: 250, width: 300, height: 100)
button.setTitle("Press to Start FadeIn", for: UIControl.State())
button.backgroundColor = .red
button.addTarget(self, action: #selector(startFadeIn), for: .touchUpInside)
view.addSubview(button)
self.view = view
}
/*
Animation in action
*/
#objc private func startFadeIn() {
label.layer.add(FadeInAdnimation(), forKey: "fadeIn")
}
}