Skip to main content
April 1, 2026Noble Desktop Publishing Team/12 min read

Card War: Animating the Cards

Master iOS Card Animation with Sequential UIView Transitions

Core Animation Concepts Covered

UIView Animation Sequencing

Learn to chain animations using completion handlers to create smooth, sequential card movements and reveals.

Transition Effects

Implement flip transitions with UIView.transition methods to create engaging card reveal animations.

Closure Integration

Master the use of self keyword in animation closures and understand capture semantics in iOS development.

Topics Covered in This iOS Development Tutorial:

This comprehensive lesson will guide you through creating sophisticated card animations that transform your basic app into an engaging, professional-quality game. You'll master animating card movement from the deck button to player positions, implementing smooth card reveal transitions, creating sequential flipping animations for dramatic effect, and integrating all animation code into a cohesive, polished experience that keeps users engaged.

Exercise Preview

ex prev card war animating cards

Exercise Overview

While your app functions correctly, exceptional user experience demands more than mere functionality—it requires engagement, polish, and the kind of attention to detail that separates professional applications from amateur efforts. In this exercise, we'll elevate your card game by implementing sophisticated animation sequences that execute code in carefully choreographed stages.

We'll construct a multi-layered animation sequence that begins by smoothly moving each player's card from the deck button to their designated positions. Following this initial movement, we'll implement sequential card flips—first revealing player 1's card with an elegant transition, then player 2's card. Only after these visual elements complete will the remaining game logic (score calculations, game state updates) execute. This approach creates anticipation, provides clear visual feedback, and demonstrates the kind of thoughtful UX design that modern users expect.

Animation Sequence Goal

Transform a static card game into an engaging experience by creating a three-phase animation: card movement from deck, sequential card reveals with flip transitions, and synchronized score updates.

Getting Started

  1. Launch Xcode if it isn't already running.

  2. If you completed the previous exercise, Card War.xcodeproj should remain open. If you closed it, reopen it now.

  3. We strongly recommend completing the previous exercises (5A–5D) before proceeding with this advanced animation work. If you did not complete the previous exercises, follow these steps:

    • Navigate to File > Open.
    • Browse to Desktop > Class Files > yourname-iOS App Dev 1 Class > Card War Ready for Animating the Cards and double-click on Card War.xcodeproj.
  4. In Xcode, ensure you have Main.storyboard, Data Model.swift, and ViewController.swift open in separate editor tabs for efficient workflow.

  5. Switch to the ViewController.swift tab to begin implementing your animations.

Project Setup Requirements

0/4

Animating the Cards' Move from the Deck Button to Their Respective Positions

In the previous exercise, we strategically wrote two code blocks that accomplish the same positioning task through different approaches. The first block stacks both cards atop the deck button, while the second instantly relocates them to their final positions above and below each player's score label.

This deliberate duplication serves a crucial purpose: it establishes both initial and final states for our animation system. By defining these two positions, we can create smooth, visually appealing transitions that guide the user's eye and create a sense of cards being "dealt" from the deck. This attention to visual storytelling is what distinguishes professional applications from basic functional implementations.

  1. The final code blocks in the drawCards function will be integrated into our animation system. To maintain clear organization while building our animation sequence, locate the following line and create several blank lines before it:

    player1CardIV.center.X = view.center.X * 0.57 + cardLayoutDistance * CGFloat(drawNumber)
  2. To initialize our animation system, we'll invoke UIView's powerful animate method. In the space where the relocated code previously resided, type UIView.animate and select the suggestion that includes a completion parameter from the autocomplete dialog:

    card war choose uiview animate

  3. Our animation will execute over half a second—a duration that feels responsive without appearing rushed. Configure the placeholder code as follows, using Tab to navigate between placeholders:

    UIView.animate(withDuration: 0.5, animations: {}, completion: { _ in })

    NOTE: The underscore (_) before the closure's in keyword indicates we're ignoring the finished boolean parameter that this closure receives—a common Swift convention for unused parameters.

  4. We'll populate both the animations and completion parameter closures with our existing code blocks. Prepare the structure by expanding both sets of curly braces:

    UIView.animate(withDuration: 0.5, animations: {
    }, completion: { _ in
    })

    NOTE: Remove any extraneous whitespace to maintain clean, readable code formatting.

  5. To populate the animations parameter, we'll relocate four specific positioning lines. Rather than cutting and pasting, we'll use Xcode's efficient line-movement commands. Begin by highlighting these four lines:

    player1CardIV.center.X = view.center.X * 0.57 + cardLayoutDistance * CGFloat(drawNumber)
    player1CardIV.center.y = view.center.y * 0.6
    player2CardIV.center.X = player1CardIV.center.X
    player2CardIV.center.y = view.center.y * 1.4
  6. Relocate the selected code block upward using Cmd–Opt–[ (Left Bracket) until it's positioned within the animations closure (Xcode will handle indentation automatically):

    UIView.animate(withDuration: 0.5, animations: {
       player1CardIV.center.X = view.center.X * 0.57 + cardLayoutDistance * CGFloat(drawNumber)
       player1CardIV.center.y = view.center.y * 0.6
       player2CardIV.center.X = player1CardIV.center.X
       player2CardIV.center.y = view.center.y * 1.4
    }, completion: { _ in
    
    })

    NOTE: The keyboard shortcut Cmd–Opt–] (Right Bracket) moves selected lines downward.

  7. You'll notice red error indicators appear on three lines. Click any red error red alert icon to see that Swift requires explicit use of the self keyword within closures.

  8. Add the required self keywords to resolve the closure capture requirements. Type them manually rather than using Fix-it suggestions for faster implementation:

    player1CardIV.center.X = self. view.center.X * 0.57 + self. cardLayoutDistance * CGFloat(self. drawNumber)
    player1CardIV.center.y = self. view.center.y * 0.6
    player2CardIV.center.X = player1CardIV.center.X
    player2CardIV.center.y = self. view.center.y * 1.4

    The self keyword is required because this code executes within a closure rather than directly in the view controller. Swift's memory management system requires explicit self references when accessing instance properties from within closures—this prevents potential retain cycles and ensures predictable behavior.

  9. We'll leave the completion block temporarily empty to observe our initial animation in isolation. Set the active scheme to iPhone 8, then click the Run button run icon to test our progress.

  10. Once the Simulator loads your app, tap the Deck button to observe the animation.

    Excellent! Your first animation is functioning perfectly. The cards smoothly transition from their initial deck position to their final game positions over the half-second duration, creating the visual impression of cards being dealt.

  11. Currently, the cards reveal their values immediately, which diminishes the suspense. Return to Xcode to implement delayed card reveals that maintain mystery until the positioning animation completes.

Card Movement Animation Implementation

1

Prepare Animation Block

Create blank lines before existing positioning code and call UIView.animate with 0.5 second duration and completion parameter.

2

Move Positioning Code

Use Cmd-Opt-[ to move the four card positioning lines into the animations block, creating smooth movement from deck to final positions.

3

Add Self Keywords

Add self keyword to view.center, cardLayoutDistance, and drawNumber references to fix closure capture errors.

4

Test Initial Animation

Run on iPhone 8 simulator to verify cards smoothly animate from deck button to their respective positions over half a second.

Closure Capture Rule

When using View Controller properties within animation closures, always explicitly reference them with self keyword to ensure proper capture semantics.

Revealing the Cards' Values After They Are Done Moving

The current implementation has a timing issue: our animation block executes in parallel with the main thread, while the remaining drawCards function code runs immediately. Since the card positioning animation takes half a second to complete, it actually runs longer than the instant image-changing code that follows it. This creates an unpolished experience where cards reveal their values before reaching their final positions.

The solution involves leveraging the animation system's completion handler—a powerful feature that ensures certain code only executes after animations finish. This approach guarantees perfect timing and creates the polished, sequential experience that users expect from professional applications.

  1. Locate the two image-assignment lines that appear after the UIView animate method:

    player1CardIV.image = player1Card.image
    player2CardIV.image = player2Card.image
  2. Use Cmd–Opt–[ (Left Bracket) to move these lines into the completion handler, ensuring the card images change only after the positioning animation completes:

    UIView.animate(withDuration: 0.5, animations: {

    Code Omitted To Save Space

    }, completion: { _ in
       player1CardIV.image = player1Card.image
       player2CardIV.image = player2Card.image
    })
  3. Test the improved timing by stopping the current simulation with the Stop button stop icon, then clicking Run run icon to restart the app.

  4. Tap the Deck button to observe the revised sequence: cards move to their positions, then both reveal simultaneously once positioning completes. While this timing improvement is solid, the instantaneous reveal lacks the dramatic flair that elevates good apps to great ones.

  5. Return to Xcode to implement the sophisticated card-flipping transitions that will create genuine excitement and anticipation in your game.

Animation Timing Issue

Animation code runs parallel to the main thread. Cards moving over 0.5 seconds take longer than instant image changes, causing premature card reveals.

Synchronizing Card Reveals

1

Move Image Code to Completion

Highlight player1CardIV.image and player2CardIV.image lines and move them into the animate method's completion handler.

2

Test Synchronized Timing

Run simulator to verify both cards reveal only after movement animation completes, though still instantaneously.

Revealing the Cards' Values with a Flipping Transition

Simultaneous card reveals, while functionally correct, miss an opportunity to build suspense and create memorable user moments. Professional game design leverages sequential reveals to maximize engagement—first showing one player's card to establish the stakes, then revealing the second card to determine the outcome.

We'll implement this using UIView's transition system, which provides sophisticated visual effects including realistic card-flipping animations. By chaining these transitions through completion handlers, we create a carefully orchestrated sequence that holds the user's attention from start to finish.

  1. Within the completion block, position your cursor before the player1CardIV.image line and add several blank lines to provide workspace for the transition code.

  2. Type UIView.transition and select the transition(with:… option from the autocomplete suggestions to generate the method template:

    }, completion: { _ in
       UIView.transition(with: UIView, duration: TimeInterval, options: UIViewAnimationOptions, animations: (() -> Void)?, completion: ((Bool) -> Void)?)

    Whitespace Between Code

    player1CardIV.image = player1Card.image
       player2CardIV.image = player2Card.image
    })
  3. Configure the transition parameters to create an engaging card-flip effect, ensuring proper closure structure:

    UIView.transition(with: player1CardIV, duration: 0.3, options: .transitionFlipFromBottom, animations: {
    }, completion: { _ in
    })
  4. Move the player1 image assignment into the transition's animations block. Position your cursor anywhere in the target line and use Cmd–Opt–[ (Left Bracket) until properly positioned:

    UIView.transition(with: player1CardIV, duration: 0.3, options:.transitionFlipFromBottom, animations: {
       player1CardIV.image = player1Card.image
    }, completion: { _ in
    })
  5. Test the enhanced animation sequence by stopping the current simulation (Stop button stop icon) then restarting (Run run icon).

  6. Tap the Deck button to observe player 1's card performing its dramatic flip animation while player 2's card reveals simultaneously. This mixed approach—one animated, one instant—creates an inconsistent user experience that we'll refine in the next steps.

  7. Tap the button again, noting that both cards reveal after the positioning animation completes, since the player 2 image code remains within the first animation's completion block.

    NOTE: If time permits, experiment with using Cmd–Opt–] (Right Bracket) to move the player2CardIV.image line outside the animate method entirely. This demonstrates how code placement affects animation timing.

  8. Return to Xcode to implement the matching flip animation for player 2's card, creating a polished, symmetrical experience.

  9. To create the second card's flip animation, we'll duplicate and modify the existing transition code. Copy these complete lines:

    UIView.transition(with: player1CardIV, duration: 0.3, options:.transitionFlipFromBottom, animations: {
       player1CardIV.image = player1Card.image
    }, completion: { _ in
    })
  10. Paste the copied code within player 1's completion handler to create the sequential chain:

    UIView.transition(with: player1CardIV, duration: 0.3, options:.transitionFlipFromBottom, animations: {
       player1CardIV.image = player1Card.image
    }, completion: { _ in
       UIView.transition(with: player1CardIV, duration: 0.3, options:.transitionFlipFromBottom, animations: {
          player1CardIV.image = player1Card.image
       }, completion: { _ in
       })
    })
  11. Modify the second transition to handle player 2's card by updating these references:

    UIView.transition(with: player2CardIV, duration: 0.3, options:.transitionFlipFromBottom, animations: {
       player2CardIV.image = player2Card.image
    }, completion: { _ in
  12. Remove any remaining duplicate player2 image assignment line that exists outside the transition code:

    player2CardIV.image = player2Card.image
  13. Stop stop icon and restart run icon the Simulator to test the complete animation sequence.

  14. Tap the Deck button to observe the full choreographed sequence: cards move into position, player 1's card flips to reveal its value, then player 2's card flips. This creates the perfect dramatic buildup that keeps users engaged throughout each hand.

  15. While monitoring a player's score label, tap the Deck button several additional times.

    You'll notice the scoring updates occur immediately upon button press—before the animations complete. This breaks the narrative flow and reveals the outcome before the visual story finishes, diminishing the impact of your carefully crafted animation sequence.

  16. Return to Xcode to implement the final integration step.

Sequential Card Flip Implementation

1

Create First Flip Transition

Add UIView.transition with player1CardIV, 0.3 duration, and transitionFlipFromBottom option. Move player1 image code into animations block.

2

Add Second Flip Transition

Copy first transition code into player1's completion handler, then edit to reference player2CardIV and player2Card for sequential flipping.

3

Clean Up Duplicate Code

Remove any remaining player2CardIV.image line outside the animation blocks to prevent duplicate image setting.

Animation Duration Breakdown

Card Movement
500
Player 1 Flip
300
Player 2 Flip
300

Incorporating the Final Code into the Animation

The remaining issue stems from game logic executing immediately rather than waiting for the visual narrative to complete. Professional user experience design demands that visual feedback and logical outcomes remain synchronized—users should see the cards reveal before learning the results.

This final integration step demonstrates a crucial principle of modern app development: visual storytelling and functional logic must work in harmony. By placing score calculations and game state updates in the final completion handler, we ensure that outcomes follow reveals, creating a coherent, satisfying user journey from action to result.

  1. Select all remaining code at the bottom of the drawCards function—this encompasses the complete game logic that should execute after visual elements complete:

    if player1Card.value > player2Card.value { player1Score += 1 }
    if player1Card.value < player2Card.value { player2Score += 1 }
    
    if drawNumber == 26 {
       deckButton.setImage(UIImage(named: "Game-Over"), for:.normal)
       gameOver = true
    }
    self.drawingCards = false
  2. Move this code block into player 2's completion handler—the deepest nested completion block that executes after all animations finish:

    Code for player2Card & Its Image View

    }, completion: { _ in
       if player1Card.value > player2Card.value { player1Score += 1 }
       if player1Card.value < player2Card.value { player2Score += 1 }
    
       if drawNumber == 26 {
          deckButton.setImage(UIImage(named: "Game-Over"), for:.normal)
          gameOver = true
       }
       self.drawingCards = false
    })
  3. Address the closure capture requirements by adding self keywords where indicated below—this ensures proper memory management and scope resolution within the nested closure environment:

    if player1Card.value > player2Card.value { self. player1Score += 1 }
    if player1Card.value < player2Card.value { self. player2Score += 1 }
    
    if self. drawNumber == 26 {
       self. deckButton.setImage(UIImage(named: "Game-Over"), for:.normal)
       self. gameOver = true
    }
    self.drawingCards = false

    This final completion block orchestrates the perfect conclusion to each round: it evaluates card values and updates scores accordingly, handles end-game state transitions when all cards are dealt, and resets the drawing flag to allow subsequent rounds. The careful sequencing ensures users experience complete visual feedback before seeing logical outcomes.

  4. Stop stop icon the current simulation and Run run icon the refined version to experience the complete, polished interaction.

  5. Tap the Deck button multiple times, observing the enforced pacing that prevents animation interruption. This deliberate timing control, implemented through the drawingCards flag at the function's start, demonstrates professional attention to edge cases and user experience consistency.

    Notice that score changes now occur only after both cards complete their flip animations—creating perfect synchronization between visual storytelling and functional outcomes. This is the hallmark of thoughtful, user-centered design.

  6. Experience the complete game by playing through all 26 rounds, or use the restart button card war restart button to reset and observe how your carefully crafted animations transform a simple card game into an engaging, professional experience that users will want to return to repeatedly.

Final Animation Integration

1

Move Game Logic to Final Completion

Highlight remaining drawCards function code including score comparisons, game over check, and drawingCards flag reset.

2

Place in Player 2 Completion Handler

Move all highlighted code into the deepest completion block (player2's card flip completion) with maximum indentation.

3

Add Required Self Keywords

Add self keyword to player1Score, player2Score, drawNumber, deckButton, gameOver, and drawingCards references.

4

Test Complete Sequence

Run simulator to verify score changes occur only after both cards flip, with proper button interaction blocking during animations.

Animation Protection

The drawingCards flag prevents user interruption by blocking rapid button clicks during the complete animation sequence.

Key Takeaways

1UIView.animate creates smooth card movements with customizable duration and completion handlers for sequential animations
2Animation closures require explicit self keyword when accessing View Controller properties due to capture semantics
3Completion handlers synchronize animation timing, ensuring code executes only after previous animations finish
4UIView.transition with transitionFlipFromBottom creates realistic card flip effects for engaging reveals
5Nested completion blocks enable complex animation sequences where each phase triggers the next
6Animation duration affects user experience - 0.5 seconds for movement and 0.3 seconds for flips provide smooth interaction
7Protection flags like drawingCards prevent user interface corruption from rapid button presses during animations
8Moving game logic into final completion handlers ensures score updates and game state changes occur at appropriate times

RELATED ARTICLES