7

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?

Hasan Baig
  • 491
  • 6
  • 17
SaifDeen
  • 862
  • 2
  • 16
  • 33
  • 1
    Why don't you figure out the color first in the loop, and use to animate outside the loop? – iphonic Mar 20 '17 at 12:28
  • The code in `completion:` is run _after_ the animation finishes. By then, the inner loop has finished (except you put a few hundred thousand items in the `colors` array). It's baffling that the compiler even lets you do this. `completion:` is an `@escaping` closure which shouldn't capture loop names I suppose oO – CodingMeSwiftly Mar 20 '17 at 12:39
  • The completion block is called asynchronously when the animation is completed, and thus is not part of the `for` loop itself. The completion handlers don't wait for each other, nor can you guarantee which order they will be triggered in. Your best bet is to call a common method from within the completion blocks that stores the first value retrieved, and ignores the others. – XmasRights Mar 20 '17 at 12:40

4 Answers4

4

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.

Frederik
  • 384
  • 3
  • 7
  • Do you have a idea how to queue them? – SaifDeen Mar 20 '17 at 13:20
  • @SaifDeen This could best be achieved by chaining them in the completion block of the _animate_ function. Perhaps you could move the call to animate inside its own function and do recursive calls to it? The method could then take another completion block, that is a function that increments and iterates through the vertexCount – Frederik Mar 20 '17 at 14:30
2

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.

Giuseppe Lanza
  • 3,519
  • 1
  • 18
  • 40
1
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)
Rajesh
  • 61
  • 5
-1

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) }
        })
}
XmasRights
  • 1,427
  • 13
  • 21