Multiple swift animations in parallel not working - ios

i have this code animating some elements (total of 3) in a view.
for element in elements{
if element.value != radians {
UIView.animateWithDuration(0.99,
animations: {
element.transform = CGAffineTransformMakeRotation(CGFloat(radians))
}, completion: {
finished in
element.value = radians
})
}
}
When 2 or more elements should be animated (UIView.animateWithDuration is called 2 or more times one after another), only one is animating and the animation is quite choppy. I know that i should write everything in the animation block, but i can't figure out how to do it.
Please help me.

Just put the for loop inside the animation block.
UIView.animateWithDuration(0.99,
animations: {
for element in elements {
if element.value != radians {
element.transform = CGAffineTransformMakeRotation(CGFloat(radians))
}
}
}, completion: { _ in
element.value = radians
})

Related

how to show array of multiple text in one label by fade in and fade out

I want to show multiple quotation in one label and at a time only one quotation appear and quotation is coming from server side in array.
I am doing this -:
for i in self.splashModel?.quotations ?? [] {
self.quoteLabel.alpha = 0
self.quoteLabel.text = i
self.quoteLabel.fadeIn(completion: {
(finished: Bool) -> Void in
self.quoteLabel.fadeOut()
})
}
this code show only last quotation of array.
You can try
self.splashModel?.quotations.enumerated().forEach { (index,item) in
DispatchQueue.main.asyncAfter(deadline: .now() + Double( index * 2) ) {
self.quoteLabel.alpha = 0
self.quoteLabel.text = item
self.quoteLabel.fadeIn(completion: {(finished: Bool) -> Void in
self.quoteLabel.fadeOut()
})
}
}

is it possible to combine two animations in Lottie framwork

swift version is 5 and lottie version is 3.1.1
I want to show two animations Json file with Lottie, that way fade in first animation and after it done it fades out and the another one fades in and I have to take a loop and do it on an infinity loop.
boardAnimationViewForSecondSlide = AnimationView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.width * (690 / 750)))
boardAnimationViewForSecondSlide?.animation = Animation.named("Slidetwop1")
slide.addSubview(boardAnimationViewForSecondSlide)
and I define a closure for handle completion play's method
private var animationState: Int = 0 // 0 first slid, 1 second slide
private var changeStateInSlide2: (Bool) -> Void = { finish in
if animationState == 0 {
boardAnimationViewForSecondSlide.animation = Animation.named("Slidetwop2.json")
playSecondPage = true
} else {
boardAnimationViewForSecondSlide.animation = Animation.named("Slidetwop1.json")
playSecondPage = true
}
}
fileprivate var playSecondPage: Bool {
get {
return false
}
set {
if newValue {
boardAnimationViewForSecondSlide.play(completion:changeStateInSlide2)
}
}
}
I think the moste simple is to create a func to launch the animation.
example :
/// Start animation with Lottie
func startAnimation(viewName: AnimationView, jsonName: String) {
viewName.isHidden = false
viewName.animation = Animation.named(jsonName)
viewName.play { (_) in
viewName.isHidden = true
}
After this you can simply call the method one after one :
startAnimation(viewName: checkAnimation, jsonName: "Slidetwop1")
startAnimation(viewName: checkAnimation, jsonName: "Slidetwop2")
Or use the completion handler to call the second.
EDIT: For the loop you can use this solution:
/// Start animation with Lottie
func startAnimation() {
animationLottieView.animation = Animation.named("Slidetwop1")
animationLottieView.play { (finished) in
// completion handler
self.animationLottieView.animation = Animation.named("Slidetwop2")
self.animationLottieView.play { (finishedAnimation) in
self.startAnimation()}
}
}

iOS (Swift): Changing UILabel text with a for loop

I have a for loop as follows:
#objc private func resetValue() {
for i in stride(from: value, to: origValue, by: (value > origValue) ? -1 : 1) {
value = i
}
value = origValue
}
And when value is set it updates a label:
private var value = 1 {
didSet {
updateLabelText()
}
}
private func updateLabelText() {
guard let text = label.text else { return }
if let oldValue = Int(text) { // is of type int?
let options: UIViewAnimationOptions = (value > oldValue) ? .transitionFlipFromTop : .transitionFlipFromBottom
UIView.transition(with: label, duration: 0.5, options: options, animations: { self.label.text = "\(value)" }, completion: nil)
} else {
label.text = "\(value)"
}
}
I was hoping that if value=5 and origValue=2, then the label would flip through the numbers 5,4,3,2. However, this is not happening - any suggestions why, please?
I've tried using a delay function:
func delay(_ delay:Double, closure: #escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
and then placing the following within the stride code:
delay(2.0) { self.value = i }
However, this doesn't seem to work either.
Thanks for any help offered.
UIKit won't be able to update the label until your code is finished with the main thread, after the loop completes. Even if UIKit could update the label after each iteration of the loop, the loop is going to complete in a fraction of a second.
The result is that you only see the final value.
When you attempted to introduce the delay, you dispatched the update to the label asynchronously after 0.5 second; Because it is asynchronous, the loop doesn't wait for the 0.5 second before it continues with the next iteration. This means that all of the delayed updates will execute after 0.5 seconds but immediately one after the other, not 0.5 seconds apart. Again, the result is you only see the final value as the other values are set too briefly to be visible.
You can achieve what you want using a Timer:
func count(fromValue: Int, toValue: Int) {
let stride = fromValue > toValue ? -1 : 1
self.value = fromValue
let timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats:true) { [weak self] (timer) in
guard let strongSelf = self else {
return
}
strongSelf.value += stride
if strongSelf.value == toValue {
timer.invalidate()
}
}
}
I would also update the didSet to send the oldValue to your updateLabelText rather than having to try and parse the current text.
private var value = 1 {
didSet {
updateLabelText(oldValue: oldValue, value: value)
}
}
private func updateLabelText(oldValue: Int, value: Int) {
guard oldValue != value else {
self.label.text = "\(value)"
return
}
let options: UIViewAnimationOptions = (value > oldValue) ? .transitionFlipFromTop : .transitionFlipFromBottom
UIView.transition(with: label, duration: 0.5, options: options, animations: { self.label.text = "\(value)" }, completion: nil)
}

How to create a closure inside a closure? [duplicate]

This question already has answers here:
Calling Swift Closure Inside Closure
(4 answers)
Closed 4 years ago.
I am trying to create a function in Swift so I can make a repeating animation (I know you can use .repeat, but I do not want to use that). In my completion closure, I am getting an error. Here is my code so far:
import UIKit
var withDurationVar:TimeInterval = 0
var optionsVar:UIViewAnimationOptions?
var iterateVar = 0
var animationsVar:(() -> ()?)?
var completionVar:(() -> ()?)?
var numberOfIterations = 0
func repeatingAnimation(withDuration:TimeInterval, options:UIViewAnimationOptions, iterate:Int, animations:#escaping () -> (), completion:#escaping () -> ()) {
withDurationVar = withDuration
optionsVar = options
iterateVar = iterate
animationsVar = animations
completionVar = completion
}
func animationRepeat() {
UIView.animate(withDuration: withDurationVar, delay: 0, options: optionsVar!, animations: animationsVar as! () -> Void, completion: { (Finished) in
// Increase number of iterations
numberOfIterations += 1
// If it has not yet done every iteration needed
if numberOfIterations != iterateVar {
// Repeat animation
animationRepeat()
}
else {
completionVar // Where I get an error. 'Expression resolves to an unused I-value'
}
})
}
I can do this however:
func animationRepeat() {
UIView.animate(withDuration: withDurationVar, delay: 0, options: optionsVar!, animations: animationsVar as! () -> Void, completion: completionVar)
}
So how can I have the completion from my repeatingAnimation function into the completion of animationRepeat with the rest of the code as well? Thanks!
I have updated my code if anyone want to use it as a repeating animation function :)
import UIKit
// Make some variables to be accessed across the file
var withDurationVar:TimeInterval = 0
var optionsVar:UIViewAnimationOptions?
var iterateVar = 0
var animationsVar:(() -> ()?)?
var completionVar:(() -> ()?)?
var numberOfIterations = 0
// The function to call
func repeatingAnimation(withDuration:TimeInterval, options:UIViewAnimationOptions, iterate:Int, animations:#escaping () -> (), completion:#escaping () -> ()) {
// Set the global variables from the parameters
withDurationVar = withDuration
optionsVar = options
iterateVar = iterate
animationsVar = animations
completionVar = completion
// Has not started repeating yet
numberOfIterations = 0
// Start repeat
animationRepeatForFunction()
}
// The function which is ONLY called by the repeatingAnimation or itself, not the user
func animationRepeatForFunction() {
UIView.animate(withDuration: withDurationVar, delay: 0, options: optionsVar!, animations: {animationsVar?()}, completion: { (Finished) in
// Increase number of iterations
numberOfIterations += 1
// If it has not yet done every iteration needed
if numberOfIterations < iterateVar {
// Repeat animation
animationRepeatForFunction()
}
else {
// Perform what the user wanted to do after the repeating animation
completionVar?()
}
})
}
Call the function using repeatingAnimation(...)
The .repeat function can only really be used for infinite loops, which do not need to be stopped. In my case, I wanted to repeat a finite amount of times (terminates), so I made this custom function which can have its own .swift file.

Break a loop inside an animation block

I'm trying to break a for-loop after a completed UIView animation. Here is the following snippet:
public func greedyColoring() {
let colors = [UIColor.blue, UIColor.green, UIColor.yellow, UIColor.red, UIColor.cyan, UIColor.orange, UIColor.magenta, UIColor.purple]
for vertexIndex in 0 ..< self.graph.vertexCount {
let neighbours = self.graph.neighborsForIndex(vertexIndex)
let originVertex = vertices[vertexIndex]
print("Checking now Following neighbours for vertex \(vertexIndex): \(neighbours)")
var doesNotMatch = false
while doesNotMatch == false {
inner: for color in colors{
UIView.animate(withDuration: 1, delay: 2, options: .curveEaseIn, animations: {
originVertex.layer.backgroundColor = color.cgColor
}, completion: { (complet) in
if complet {
let matches = neighbours.filter {
let vertIdx = Int($0)!
print("Neighbour to check: \(vertIdx)")
let vertex = self.vertices[vertIdx-1]
if vertex.backgroundColor == color{
return true
}else{
return false
}
}
//print("there were \(matches.count) matches")
if matches.count == 0 {
// do some things
originVertex.backgroundColor = color
doesNotMatch = true
break inner
} else {
doesNotMatch = false
}
}
})
}
}
}
}
Basically this method iterate over a Graph and checks every vertex and its neighbours and give the vertex a color that none of its neighbours has. Thats why it has to break the iteration at the first color that hasn't been used. I tried to use Unlabeled loops but it still doesn't compile (break is only allowed inside a loop). The problem is that I would like to visualise which colors have been tested. Thats why I'm using the UIView.animate()
Is there anyway to solve my problem?
You need to understand, that the completion block that you pass to the animate function is called after the animation has finished, which is a long time (in computer time) after your for loop has iterated through the colors array. You set the duration to be 1 second, which means that the completion is called 1 second later. Since the for loop isn't waiting for your animation to finish, they will all start animating at the same time (off by some milliseconds perhaps). The for loop has completed way before the animation has completed, which is why it doesn't make sense to break the for loop, since it is no longer running!
If you want to see this add a print("Fire") call right before the UIView.animate function is called and print("Finished") in the completion block. In the console you should see all the fire before all the finished.
You should instead queue the animations, so that they start and finish one after the other.
As Frederik mentioned, the animations the order in execution of the next in your for loop is not synchronous with the order of your completions. This means that the for loop will keep cycling no matter of your blocks implementations.
An easy fix would be to create the animations by using CABasicAnimation and CAAnimationGroup. So that the product of your for loop would be a chain of animations stacked in an animation group.
This tutorial will give you an idea on how to use CABasicAnimation and CAAnimationGroup: https://www.raywenderlich.com/102590/how-to-create-a-complex-loading-animation-in-swift
You can use this approach, because the condition to break your for loop is given by parameters that are not dependent by the animation itself. The animations will be executed anyway once they will be attached to the view you are trying to animate.
Hope this helps.
Just modifying your code a little bit. Its a old school recursion but should work. Assuming all the instance variables are available here is a new version.
public func greedyColoring(vertex:Int,colorIndex:Int){
if vertexIndex > self.graph.vertexCount { return }
if colorIndex > color.count {return}
let neighbours = self.graph.neighborsForIndex(vertexIndex)
let originVertex = vertices[vertexIndex]
let color = self.colors[colorIndex]
UIView.animate(withDuration: 1, delay: 2, options: .curveEaseIn, animations: {
originVertex.layer.backgroundColor = color.cgColor
}, completion: { (complet) in
if complet {
let matches = neighbours.filter {
let vertIdx = Int($0)!
print("Neighbour to check: \(vertIdx)")
let vertex = self.vertices[vertIdx-1]
//No idea what you are trying to do here. So leaving as it is.
if vertex.backgroundColor == color{
return true
}else{
return false
}
}
//print("there were \(matches.count) matches")
if matches.count == 0 {
// do some things
originVertex.backgroundColor = color
greedyColoring(vertex: vertex++,colorIndex:0)
} else {
greedyColoring(vertex: vertex,colorIndex: colorIndex++)
}
}
})
}
Now we can call this function simply
greedyColor(vertex:0,colorIndex:0)
As mentioned before, the completion blocks are all called asynchronously, and thus do not exist within the for loop. Your best bet is to call a common method from within the completion blocks which will ignore everything after the first call.
var firstMatch: UIColor?
func foundMatch (_ colour: UIColor)
{
if firstMatch == nil
{
firstMatch = colour
print (firstMatch?.description ?? "Something went Wrong")
}
}
let colours: [UIColor] = [.red, .green, .blue]
for colour in colours
{
UIView.animate(withDuration: 1.0, animations: {}, completion:
{ (success) in
if success { foundMatch (colour) }
})
}

Resources