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()
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.