iosswiftanimationuiviewpropertyanimator

How do you sequentially animate an array of image views in Swift?


I've been trying to figure this out for a while to no avail. Basically, I have an array of UIImageViews that I want to animate 1-by-1 in order. Right now, they all animate correctly but simultaneously. Additionally, I want the code at the bottom to execute only AFTER all the animations are done, as right now it seems to be executed at the same time. I'm pretty new to swift so I guess I don't fully understand how for loops work? In my mind this code should loop through all the image views in the array and only after it's done should it execute the code at the bottom. the highlightCards method should be called on each iteration of the loop but that also doesn't seem to be happening. I know the arrays are initiated correctly as the correct images are being moved.

Here's the code:

playedCards is an array of ints with card indexes

game.board is an array of card objects (UIImageView)

highlightCards() just highlights cards that are currently playable

The code in the completion block is just for logistics and I know it's working correctly

The code under "set up new game" comment is what should be executed only after all the animations are completed

for i in 0..<game.playedCards.count {
    // update highlights
    self.highlightCards()

    // animate cards off screen
    let endPoint: CGPoint = CGPoint(x: 1000, y: 250 )

    UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 3, delay: 2, options: UIView.AnimationOptions.curveEaseInOut, animations: {
        () -> Void in
        self.game.board[self.game.playedCards[i]].center = endPoint
    }, completion: {
        (Bool) -> Void in
        self.game.board[self.game.playedCards[i]].removed = true
        self.game.board[self.game.playedCards[i]].removeFromSuperview()
        self.currentCardCount = self.currentCardCount - 1
    })
}

// set up next turn
game.blueTurn = !game.blueTurn
self.setBackground()
self.setSuitIndicators()
self.highlightCards()

Solution

  • You're on the right track! The only problem here is that the animation function runs asynchronously – meaning that it won't wait until the previous thing has finished animating before going to the next loop.

    You're already part of the way there in using the completion closure. However, if you want to wait to animate the next item after the completion of the previous one, you'll essentially have to find a way to run your property animator again from inside the completion block.

    The best way to do this is by extracting your property animation call to a function. Then it makes this easy!

    // This is a function that contains what you've included in your code snippet.
    func animateCards() {
    
        // update highlights
        highlightCards()
    
        // animate cards off screen
        let endPoint: CGPoint = CGPoint(x: 1000, y: 250)
        recursivelyAnimateCard(at: game.playedCards.startIndex, to: endPoint) {
    
            //set up next turn
            self.game.blueTurn = !self.game.blueTurn
            self.setBackground()
            self.setSuitIndicators()
            self.highlightCards()
        }
    }
    
    // This function replicates your for-loop that animates everything.
    func recursivelyAnimateCard(at index: Int, to endPoint: CGPoint, completion: () -> Void) {
    
        // If this triggers, we've iterated through all of the cards. We can exit early and fire off the completion handler.
        guard game.playedCards.indices.contains(index) else {
            completion()
            return 
        }
    
        UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 3, delay: 2, options: UIView.AnimationOptions.curveEaseInOut, 
        animations: {
            self.game.board[self.game.playedCards[index]].center = endPoint
        }, 
        completion: { _ in
            self.game.board[self.game.playedCards[index]].removed = true
            self.game.board[self.game.playedCards[index]].removeFromSuperview()
            self.currentCardCount = self.currentCardCount - 1
    
            // Call this function again on completion, but for the next card.
            self.recursivelyAnimateCard(at: index + 1, to: endPoint, completion: completion)
        })
    }
    

    EDIT: I hadn't seen your need to run the "next turn" code after the whole thing completed. I've updated the code above to contain a completion handler that fires off after all of the cards have been removed from the screen.