I have a very strange issue in my app. I'm using UIViewPropertyAnimator to animate changing images inside UIImageView.
Sounds like trivial task but for some reaso my view ends up changing the image instantly so I end up with images flashing at light speed instead of given duration and delay parameters.
Here's the code:
private lazy var images: [UIImage?] = [
UIImage(named: "widgethint"),
UIImage(named: "shortcuthint"),
UIImage(named: "spotlighthint")
]
private var imageIndex = 0
private var animator: UIViewPropertyAnimator?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animator = repeatingAnimator()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
animator?.stopAnimation(true)
}
private func repeatingAnimator() -> UIViewPropertyAnimator {
return .runningPropertyAnimator(withDuration: 2, delay: 6, options: [], animations: {
self.imageView.image = self.images[self.imageIndex]
}, completion: { pos in
if self.imageIndex >= self.images.count - 1 {
self.imageIndex = 0
} else {
self.imageIndex += 1
}
self.animator = self.repeatingAnimator()
})
}
So, according to the code animation should take 2 seconds to complete and start after 6 seconds delay, but it starts immediately and takes milliseconds to complete so I end up with horrible slideshow. Why could it be happening?
I also tried using the UIViewPropertyAnimator(duration: 2, curve: .linear) and calling repeatingAnimator().startAnimation(afterDelay: 6) but the result is the same.
Okay, I've figured it out, but it's still a bit annoying that such simple task cannot be done using the "Modern" animation API.
So, animating images inside UIImageView is apparently not supported by UIViewPropertyAnimator, during debugging I tried animating view's background color and it was working as expected. So I had to use Timer instead and old UIView.transition(with:) method
Here's the working code:
private let animationDelay: TimeInterval = 2.4
private var animationTimer: Timer!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setAnimation(true)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
setAnimation(false)
}
private func setAnimation(_ enabled: Bool) {
guard enabled else {
animationTimer?.invalidate()
animationTimer = nil
return
}
animationTimer = Timer.scheduledTimer(withTimeInterval: animationDelay, repeats: true) { _ in
UIView.transition(with: self.imageView, duration: 0.5, options: [.curveLinear, .transitionCrossDissolve], animations: {
self.imageView.image = self.images[self.imageIndex]
}, completion: { succ in
guard succ else {
return
}
if self.imageIndex >= self.images.count - 1 {
self.imageIndex = 0
} else {
self.imageIndex += 1
}
})
}
}
Hopefully it will save someone some headaches in the future
Related
I am playing around to learn and implemented a progressView that is triggered using a function and reset using another one when buttons are clicked (and feed it with value). For some reason, after the first click, it works fine even though the functions are called also on the following clicks and the progressView is reset, its animation is not restarting (checked functions and values are all correct using print()...)
import UIKit
class ViewController: UIViewController {
let eggTimes = ["Soft" : 5, "Medium" : 7, "Hard": 12]
#IBOutlet weak var progressView: UIProgressView!
//reset the progress bar
func resetProgress(){
progressView.setProgress(0, animated: true)
print("function progress view reset fired")
}
//starts the prgress bar
func startProgressView(duration: Double) {
UIView.animate(withDuration: duration) {
print("progress bar has been initiated")
self.progressView.setProgress(1.0, animated: true)
}
}
#IBAction func hardnessSelected(_ sender: UIButton) {
resetProgress()
print("progress bar has been reset")
let hardness = sender.currentTitle!
var timerCounter = eggTimes[hardness]! * 10
let temp1: Double = Double(timerCounter)
print("temp 1 = \(temp1) ")
self.startProgressView(duration: temp1)
print(eggTimes[hardness]!)
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
if timerCounter > 0 {
timerCounter -= 1
// print(timerCounter)
}
else {
print("egg ready")
timer.invalidate()
}
}
}
}
I am attempting to animate the alpha of a UIImage after 3 seconds has passed. By default the alpha is set to 0 and after 3 seconds, the alpha should change to 1, thus displaying the image to the user. My code I wrote for my animation does set the alpha to 0, but I am unable to change the the alpha to 1 after 3 seconds. I am newer to swift and not sure where I am going wrong with this. Here is my code.
import UIKit
class WelcomeOneViewController: UIViewController {
#IBOutlet weak var swipeImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
swipeImageView.alpha = 0
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
displaySwipeView()
}
func displaySwipeView() {
UIView.animate(withDuration: 1.0, delay: 3.0, options: .beginFromCurrentState, animations: {
DispatchQueue.main.async { [weak self] in
self?.swipeImageView.alpha = 1
}
}, completion: nil)
}
}
Try it with this code:
import UIKit
class WelcomeOneViewController: UIViewController {
#IBOutlet weak var swipeImageView: UIImageView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
swipeImageView.alpha = 0
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
displaySwipeView()
}
func displaySwipeView() {
swipeImageView.alpha = 0
UIView.animate(withDuration: 1.0, delay: 3.0, options: [], animations: {
self.swipeImageView.alpha = 1
}, completion: nil)
}
}
Try doing it on main queue like this:
func displaySwipeView() {
UIView.animate(withDuration: 1.0, delay: 3.0, options: .beginFromCurrentState, animations: {
DispatchQueue.main.async { [weak self] in
self?.swipeImageView.alpha = 1
}
}, completion: nil)
}
Hope it helps!!
I don't know how to stop this call without using the timer concept. Today, when trying to profile the project I came across this allocation memory get increasing every time because of following function:
func startAnimation(index: Int) {
UIView.animate(withDuration: 4.0, delay: 0.5, options:[UIViewAnimationOptions.allowUserInteraction, UIViewAnimationOptions.curveEaseInOut], animations: {
self.view.backgroundColor = self.colors[index]
}) { (finished) in
var currentIndex = index + 1
if currentIndex == self.colors.count { currentIndex = 0 }
self.startAnimation(index: currentIndex)
}
}
Do one simple thing, mention one flag,
Initially it should,
var isViewDissappear = false
Then,
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
isViewDissappear = true
}
Then check that flag while giving call again,
func startAnimation(index: Int) {
UIView.animate(withDuration: 4.0, delay: 0.5, options:[UIViewAnimationOptions.allowUserInteraction, UIViewAnimationOptions.curveEaseInOut], animations: {
self.view.backgroundColor = self.colors[index]
}) { (finished) in
var currentIndex = index + 1
if currentIndex == self.colors.count { currentIndex = 0 }
if !self.isViewDissappear {
self.startAnimation(index: currentIndex)
}
}
}
that's it.
You are making a retain cycle by capturing strong reference to self to the animation closure. Which means that your self is owned by the closure, and closure owned by self - this causes a leak mentioned by you (increase of memory usage). You can break the cycle by capturing weak reference to self by using capture list (read the docs). Try this:
func startAnimation(index: Int) {
UIView.animate(withDuration: 4.0, delay: 0.5, options:[UIViewAnimationOptions.allowUserInteraction, UIViewAnimationOptions.curveEaseInOut], animations: { [weak self]
self?.view.backgroundColor = self?.colors[index]
}) { (finished) in
var currentIndex = index + 1
if currentIndex == self?.colors.count { currentIndex = 0 }
self?.startAnimation(index: currentIndex)
}
}
So i built a fade in fade out for a label animation in Swift already. But I want to fade in and out from an array of different sentences, so each time fade in and fade out, it would be different staff.
override func viewDidLoad()
{
super.viewDidLoad()
label.alpha = 0
animatedText()
}
func animatedText(){
UIView.animateWithDuration(2.0, animations: {
self.label.alpha = 1.0
}, completion: {
(Completed: Bool) -> Void in
UIView.animateWithDuration(1.0, delay: 2.0, options: UIViewAnimationOptions.CurveLinear, animations: {
self.label.alpha = 0
}, completion: {
(Completed: Bool) -> Void in
self.animatedText()
})
})
}
Try this function:
class ViewController: UIViewController {
#IBOutlet weak var label1: UILabel!
#IBOutlet weak var label2: UILabel!
var sentences = ["The cat sat.", "The rhino ate.", "The monkey laughed", "The zebra ran.", "The fish swam", "The potato grew."]
override func viewDidLoad() {
super.viewDidLoad()
label1.text = sentences[0]
label2.text = sentences[1]
label1.alpha = 1.0
label2.alpha = 0.0
}
var counter = 1
#IBAction func animateBtnPressed(_ sender: UIButton) {
fadeText(counter: &counter, duration: 2.0)
}
func fadeText(counter: inout Int, duration: TimeInterval) {
if counter < sentences.count {
if counter % 2 != 0 {
UIView.animate(withDuration: duration, animations: {
self.label1.alpha = 0.0
self.label2.alpha = 1.0
}, completion: { finished in
if finished {
counter += 1
if counter <= (self.sentences.count - 1) {
self.label1.text = self.sentences[counter]
} else {
return
}
self.fadeText(counter: &counter, duration: duration)
}
})
} else {
UIView.animate(withDuration: duration, animations: {
self.label1.alpha = 1.0
self.label2.alpha = 0.0
}, completion: { finished in
if finished {
counter += 1
if counter <= (self.sentences.count - 1) {
self.label2.text = self.sentences[counter]
} else {
return
}
self.fadeText(counter: &counter, duration: duration)
}
})
}
}
}
}
Assumptions:
There is an array named sentences (you can alter this) on the ViewController that contains Strings.
There is a countervariable that starts at 1
The UILabels are overlapping in the storyboard.
Other than that you just have to call the function with the counter variable with an & operator in front, because for fadeText to alter the counter variable it needs the address, in memory, of the counter variable, not the value (notice the inout in the parameters of fadeText).
If you have any questions feel free to ask!
I would advise creating an int variable outside the scope of the animatedText() function and simply adding 1 to this variable each time your completion block is called. You can then update the label's text to the string in the array using this int variable as the array's index each time the function is called.
I want to make my UILabels appear in a delayed sequence. Thus one after another.
The code below works when I make them fade in using the alpha value but it doesn't do what I want it to do when I use the .hidden property of the UILabels.
The code makes my UILabels appear at all the same time instead of sum1TimeLabel first after 5 seconds, sum2TimeLabel second after 30 seconds and finally sum3TimeLabel third after 60 seconds. What am I doing wrong?
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(5.0, animations: {
self.sum1TimeLabel!.hidden = false;
})
UIView.animateWithDuration(30.0, animations: {
self.sum2TimeLabel!.hidden = false;
})
UIView.animateWithDuration(60.0, animations: {
self.sum3TimeLabel!.hidden = false;
})
}
As explained by SO user #matt in this answer, you can simply create a delay function with Grand Central Dispatch instead of using animateWithDuration, as animateWithDuration is usually meant for animating things over a period of time, and since the value you are animating is a Bool, it can't animate it.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
delay(5.0) {
self.sum1TimeLabel?.hidden = false
}
delay(30.0) {
self.sum2TimeLabel?.hidden = false
}
delay(60.0) {
self.sum3TimeLabel?.hidden = false
}
}
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
// Try this
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.sum1TimeLabel!.hidden = true;
self.sum2TimeLabel!.hidden = true;
self.sum3TimeLabel!.hidden = true;
UIView.animateWithDuration(5.0, animations: {
}, completion: {
self.sum1TimeLabel!.hidden = false;
})
UIView.animateWithDuration(30.0, animations: {
}, completion: {
self.sum2TimeLabel!.hidden = false;
})
UIView.animateWithDuration(60.0, animations: {
}, completion: {
self.sum3TimeLabel!.hidden = false;
})
}
class ViewController: UIViewController {
#IBOutlet weak var label1: UILabel!
#IBOutlet weak var label2: UILabel!
#IBOutlet weak var label3: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
label1.alpha = 0
label2.alpha = 0
label3.alpha = 0
UIView.animateWithDuration(5.0, animations: { () -> Void in
print("animation on label1")
self.label1.alpha = 0.2
}) { (b) -> Void in
print("complete on label1")
self.label1.alpha = 1
UIView.animateWithDuration(5.0, animations: { () -> Void in
print("animation on label2")
self.label2.alpha = 0.2
}, completion: { (b) -> Void in
print("complete on label2")
self.label2.alpha = 1
UIView.animateWithDuration(5.0, animations: { () -> Void in
print("animation on label3")
self.label3.alpha = 0.2
}, completion: { (b) -> Void in
print("complete on label3")
self.label3.alpha = 1
})
})
}
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
You are NOT able to animate hidden property. there is two states only. how to change hidden continuously with time? instead you can animate alpha :-). try change the alpha value inside animation block to 0, or to 1.