I have a for loop that executes animations and then removes them on completion, and I am trying to call another method upon completion. Here is my code:
func animateMatch(completion: () -> ()) {
var movingShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
let destinationShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
for everyShape in 1..<pointsContainingShapeArray.count {
movingShape = level.shapeAtColumn(pointsContainingShapeArray[everyShape].Column, row: pointsContainingShapeArray[everyShape].Row)
let Duration: NSTimeInterval = 1
let moveShapes = SKAction.moveTo((destinationShape?.sprite!.position)!,duration: Duration)
moveShapes.timingMode = .EaseOut
movingShape?.sprite?.runAction(moveShapes, completion: {
movingShape?.sprite?.removeFromParent()
print("Removed shape \(everyShape)")
if everyShape == pointsContainingShapeArray.count {
completion()
}
})
}
}
Basically the idea is that every shape in the array after the first position moves to the position of the first one and then removes it from the scene. This works fine, but my completion was getting called at random times every time. So finally I added the print statement in there. Here was the output:
Removed shape 3
Removed shape 1
Removed shape 4
Removed shape 2
At this point I got so frustrated I decided it was time for stack overflow. I can't possibly figure out what's going on here. Thanks for any help!
Here is the code that calls this method:
func handleSwipe(move: Move) {
view.userInteractionEnabled = false
level.performMove(move)
scene.animateMove(move) {
self.view.userInteractionEnabled = true
if hasMatch {
self.view.userInteractionEnabled = false
self.scene.animateMatch() {
self.scene.addNewSpritesForShapes(pointsContainingShapeArray)
hasMatch = false
self.view!.userInteractionEnabled = true
}
}
}
}
The issue isn't the for loop, it's the asynchronous nature of the run action.
When you call moveShapes action, it runs asynchronously before calling the completion back on the original thread. This can happen in any order. You can see this yourself by calling your print synchronously:
for everyShape in 1..<pointsContainingShapeArray.count {
print("Synchronous loop: \(everyShape)")
}
I think you'd be better off using a dispatch_group_t for your final completion:
func animateMatch(completion: () -> ()) {
var movingShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
let destinationShape = level.shapeAtColumn(pointsContainingShapeArray[0].Column, row: pointsContainingShapeArray[0].Row)
// HERE
let group = dispatch_group_create()
dispatch_group_notify(group, dispatch_get_main_queue(), completion)
//
for everyShape in 1..<pointsContainingShapeArray.count {
// HERE
dispatch_group_enter(group)
//
movingShape = level.shapeAtColumn(pointsContainingShapeArray[everyShape].Column, row: pointsContainingShapeArray[everyShape].Row)
let Duration: NSTimeInterval = 1
let moveShapes = SKAction.moveTo((destinationShape?.sprite!.position)!,duration: Duration)
moveShapes.timingMode = .EaseOut
movingShape?.sprite?.runAction(moveShapes, completion: {
movingShape?.sprite?.removeFromParent()
print("Removed shape \(everyShape)")
// HERE
dispatch_group_leave(group)
//
})
}
}
Related
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()}
}
}
I have built this app with the help of some friends. I don't really know how the code works.
Basically using an apple pencil it records data (time on tablet, speed of apple pencil, stroke counts etc). However as more time elapses and more drawing occurs, the timer gets out of sync with real time.
The purpose of this app is for dementia research, I get patients to draw on the tablet, and i collect information of that. I can't do the research if the timer stinks.
I have tried disabling all the timers, but the lag remains the same. I have a felling it has something to do with how strokes are being sampled. I just need a stroke count I don't need it to show strokes per min (which is what it is currently doing). I think the stroke counter might the cause???
this is the program:
https://drive.google.com/open?id=1lwzKwG7NLcX1qmE5yoxsdq5HICV2TNHm
class StrokeSegment {
var sampleBefore: StrokeSample?
var fromSample: StrokeSample!
var toSample: StrokeSample!
var sampleAfter: StrokeSample?
var fromSampleIndex: Int
var segmentUnitNormal: CGVector {
return segmentStrokeVector.normal!.normalized!
}
var fromSampleUnitNormal: CGVector {
return interpolatedNormalUnitVector(between: previousSegmentStrokeVector, and: segmentStrokeVector)
}
var toSampleUnitNormal: CGVector {
return interpolatedNormalUnitVector(between: segmentStrokeVector, and: nextSegmentStrokeVector)
}
var previousSegmentStrokeVector: CGVector {
if let sampleBefore = self.sampleBefore {
return fromSample.location - sampleBefore.location
} else {
return segmentStrokeVector
}
}
var segmentStrokeVector: CGVector {
return toSample.location - fromSample.location
}
var nextSegmentStrokeVector: CGVector {
if let sampleAfter = self.sampleAfter {
return sampleAfter.location - toSample.location
} else {
return segmentStrokeVector
}
}
init(sample: StrokeSample) {
self.sampleAfter = sample
self.fromSampleIndex = -2
}
#discardableResult
func advanceWithSample(incomingSample: StrokeSample?) -> Bool {
if let sampleAfter = self.sampleAfter {
self.sampleBefore = fromSample
self.fromSample = toSample
self.toSample = sampleAfter
self.sampleAfter = incomingSample
self.fromSampleIndex += 1
return true
}
return false
}
}
class StrokeSegmentIterator: IteratorProtocol {
private let stroke: Stroke
private var nextIndex: Int
private let sampleCount: Int
private let predictedSampleCount: Int
private var segment: StrokeSegment!
init(stroke: Stroke) {
self.stroke = stroke
nextIndex = 1
sampleCount = stroke.samples.count
predictedSampleCount = stroke.predictedSamples.count
if (predictedSampleCount + sampleCount) > 1 {
segment = StrokeSegment(sample: sampleAt(0)!)
segment.advanceWithSample(incomingSample: sampleAt(1))
}
}
func sampleAt(_ index: Int) -> StrokeSample? {
if index < sampleCount {
return stroke.samples[index]
}
let predictedIndex = index - sampleCount
if predictedIndex < predictedSampleCount {
return stroke.predictedSamples[predictedIndex]
} else {
return nil
}
}
func next() -> StrokeSegment? {
nextIndex += 1
if let segment = self.segment {
if segment.advanceWithSample(incomingSample: sampleAt(nextIndex)) {
return segment
}
}
return nil
}
}
for example at true 25 seconds, the app displays the total time at 20 seconds.
A Timer is not something to count elapsed time. It is a tool used to trigger an execution after some time has elapsed. But just "after" some time has elapsed, not "exactly after" some time has elapsed. So for instance doing something like:
var secondsElapsed: TimeInterval = 0.0
let timeInitiated = Date()
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
secondsElapsed += 1
print("\(secondsElapsed) seconds should have passed but in reality \(Date().timeIntervalSince(timeInitiated)) second have passed")
}
you will see that the two are not the same but are pretty close. But as soon as I add some extra work like this:
var secondsElapsed: TimeInterval = 0.0
let timeInitiated = Date()
func countTo(_ end: Int) {
var string = ""
for i in 1...end {
string += String(i)
}
print("Just counted to string of lenght \(string.count)")
}
Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { _ in
countTo(100000)
}
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
secondsElapsed += 1
print("\(secondsElapsed) seconds should have passed but in reality \(Date().timeIntervalSince(timeInitiated)) second have passed")
}
we get to situations like "14.0 seconds should have passed but in reality 19.17617702484131 second have passed".
We made the application busy so it doesn't have time to count correctly.
In your case you will need to use one of two solutions:
If you are interested in time elapsed simply use timeIntervalSince as demonstrated in first code snippet.
If you need to ensure triggering every N seconds you should optimize your code, consider multithreading... But mostly keep in mind that you can only get close to "every N seconds", it should not be possible to guarantee an execution exactly every N seconds.
Simply, I have a button that adds to a queue, or I believe that's how it works - I've only just started to use GCD. If I press the button twice in my app, it runs these two processes in parallel and the progress bar races back and forth between them, I'd like to stop all the other instances of the queue and execute a new one if that makes sense, I've not got to grips with the lingo...
#IBAction func makePi(_ sender: UIButton) {
piProgress.progress = 0.0
let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.userInitiated)
queue1.async {
let currentAmount = Int(self.randNum.value)
var gcdCounter = 0
for n in 1...currentAmount {
if self.gcd(self.random(max: Int(self.randRange.value)), self.random(max: Int(self.randRange.value))) == 1{
gcdCounter += 1
}
if n % 1000 == 0 {
DispatchQueue.main.async {
self.piProgress.setProgress(Float(Double(n)/Double(currentAmount)), animated: true)
}
} else if n == currentAmount {
DispatchQueue.main.async {
self.piProgress.setProgress(1.0, animated: true)
}
}
}
let piFinal = sqrt(Double((6/Double((Double(gcdCounter)/Double(currentAmount))))))
let roundedAccuracy: Double = ((piFinal-Double.pi)/Double.pi*100).roundTo(places: 6)
DispatchQueue.main.async {
self.piLabel.text = String(piFinal)
self.piAccuracy.text = "Accuracy: \(roundedAccuracy)%"
}
}
}
I just need all the other queue1.async ones to stop...
P.s. If I've done anything obviously wrong, please help ^^ Thanks.
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) }
})
}
Objective: Apply a single SKAction Animation on several nodes using a loop, while waiting for completion handler.
func animateShuffle(cards: [Card], completionHandler:(success: Bool) -> Void) {
var rotationInRadians:CGFloat = CGFloat(M_PI)
var rotateFirstDeck = SKAction.rotateByAngle(rotationInRadians, duration: 1.0)
var moveFirstDeck = SKAction.moveToX(-150, duration: 1.0)
var returnToOrigPosition = SKAction.moveToX(-125, duration: 1.0)
var firstDeckSequence = SKAction.sequence([moveFirstDeck, rotateFirstDeck, returnToOrigPosition])
var rotateSecondDeck = SKAction.rotateByAngle(-rotationInRadians, duration: 1.0)
var moveSecondDeck = SKAction.moveToX(-100, duration: 1.0)
var secondDeckSequence = SKAction.sequence([moveSecondDeck, rotateSecondDeck, returnToOrigPosition])
for (index, Card) in enumerate(cards){
if index % 2 == 0{
Card.image.runAction(firstDeckSequence)
}
else{
Card.image.runAction(secondDeckSequence)
}
}
//hack
cards[0].image.runAction(firstDeckSequence, completion: { () -> Void in
completionHandler(success: true)
})
}
Here I call the function which kicks off another Animation:
animateShuffle(newDeck.deck, completionHandler: { (success) -> Void in
print("entered")
var drawnCards = [Card]()
if let areThereCards = newDeck.drawCards(4){
drawnCards = areThereCards
}
self.dealCards(drawnCards)
})
I used a hack to get to 99% of where I want to be but my gut tells me there is better way to do this. How can I tell when the last node has finished animating?
Both sequences run for 3 seconds so when the first completion block runs you can safely assume they are all done. To be 100% certain set a bool ivar in the completion handler and then run your post-sequence code in the didEvaluateActions method of the scene.
Alternatively count how many sequences you run, then increase a counter in completion block, and run your code when both counters match.