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

Card War: Displaying the Cards & Score

Build Interactive Card Game UI with Swift Programming

Core Development Components

UIImageView Implementation

Create dynamic card display containers that hold and manage card images throughout gameplay. Essential for visual card representation.

Programmatic Constraints

Set precise positioning and sizing for UI elements using code instead of Storyboard. Provides dynamic layout control.

Game State Management

Handle scoring, card drawing, and game restart functionality. Critical for maintaining proper game flow and user experience.

Topics Covered in This iOS Development Tutorial:

Creating the UIImageViews That Will Hold the Cards, Setting the Size & Position of the Cards That Are Drawn, Revealing the Cards & Updating the Winner's Score, Defining What Happens When the Game is Restarted

Exercise Preview

ex prev card war displaying cards

Exercise Overview

This exercise marks a pivotal moment in your iOS development journey—you'll finally bring your Card War app to life in the Simulator. Up to this point, we've built the foundational architecture, but now we're ready to create the visual elements that make the game engaging and interactive.

To display the cards when the app runs, we'll construct a sophisticated system of UIImageViews that store the two cards drawn in each round and assign them to their respective players. These image views require precise sizing and positioning to match the Deck button's dimensions while maintaining proper spacing within the view's constraints. We'll also implement the scoring logic to update the winning player's score after each round and add functionality for game resets—essential features for any polished gaming experience.

Development Workflow Overview

1

Create Card Containers

Build UIImageViews to hold the card images for both players during gameplay rounds

2

Position Elements

Set programmatic constraints to properly size and position cards relative to existing UI elements

3

Implement Game Logic

Add scoring system, card reveal functionality, and game restart capabilities

Getting Started

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

  2. If you completed the previous exercise, Card War.xcodeproj should still be open. If you closed it, re-open it now.

  3. We strongly recommend completing the previous exercises (5A–5C) before proceeding, as they establish the core functionality this exercise builds upon. If you did not complete the previous exercises, follow these steps:

    • Go to File > Open.
    • Navigate to Desktop > Class Files > yourname-iOS App Dev 1 Class > Card War Ready for Displaying 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. Make sure you are in the ViewController.swift tab to begin coding.

Prerequisites Required

Complete previous exercises 5A-5C before proceeding. Ensure Main.storyboard, Data Model.swift, and ViewController.swift are open in separate tabs for efficient development workflow.

Creating the UIImageViews That Will Hold the Cards

In the previous exercise, we declared constants that assign the topmost card in the shuffled deck to each player. While we now have a mechanism to store each player's drawn card, we lack the display logic to show these cards in our interface. Let's implement the visual representation system that will finally allow us to see our app in action.

The approach we'll take leverages Apple's UIImageView class, which provides optimized image rendering capabilities essential for smooth gameplay. We'll initially display card backs to maintain the element of surprise—a crucial aspect of card games that builds anticipation before the reveal.

  1. At the end of the drawCards function before its final curly brace, create the UIImageView that will display the image corresponding to the first player's card:

    let player2Card = deck.shuffledDeck.removeLast()
    
       let player1CardIV = UIImageView(image: UIImage(named: "Card-Back"))
    
    }

    Notice that we're using Apple's UIImageView class to display an image in our user interface. To enhance the element of surprise that makes games of chance compelling, we're initially assigning the card back image rather than revealing the actual drawn card. In a later step, we'll create an animated transition that flips the card over to reveal the true image—a detail that significantly improves user experience.

  2. Near the top of the ViewController class and under the @IBOutlet variables, locate the following variable that maintains a reference to each generated card so we can remove them from the superView when the game resets:

    var cardsImageViews = [UIImageView]()

    The IV suffix in our player1CardIV constant stands for Image View, indicating it generates a UIImageView class object. The cardsImageViews array serves as a cleanup mechanism, ensuring we can efficiently remove all image views when they're no longer needed. This memory management approach prevents potential performance issues as the game progresses.

  3. Back at the end of the drawCards function, integrate the cardsImageViews variable with the player1CardIV constant:

    let player1CardIV = UIImageView(image: UIImage(named: "Card-Back"))
       cardsImageViews.append(player1CardIV)
    
    }

    This appends player 1's card image view to the cardsImageViews array. When the user resets the game, this array gets emptied and the image view is removed from the superView—think of it as calling in the "cleanup crew" to maintain optimal app performance.

  4. Copy the following two lines of code, then paste them directly below the originals:

    let player1CardIV = UIImageView(image: UIImage(named: "Card-Back"))
    cardsImageViews.append(player1CardIV)
  5. Modify the pasted code to reference player 2 by changing the highlighted portions:

    let player1CardIV = UIImageView(image: UIImage(named: "Card-Back"))
    cardsImageViews.append(player1CardIV)
    let player2CardIV = UIImageView(image: UIImage(named: "Card-Back"))
    cardsImageViews.append(player2CardIV)
To deepen the element of surprise that makes games of chance so exciting, we're assigning the image of the card back, NOT the front image of the actual card that was drawn.
This design decision creates anticipation and enables animated card reveal transitions in future development phases.
Image View Management

The cardsImageViews array maintains handles to all generated cards, enabling proper cleanup when the game resets. The IV suffix stands for Image View, following iOS naming conventions.

Setting the Size & Position of the Cards That Are Drawn

With our UIImageViews created, we need to define their size and position programmatically. This approach mirrors setting constraints in Storyboard but gives us greater dynamic control over the layout. We'll ensure the cards maintain consistent dimensions with the Deck button while positioning them optimally within the view hierarchy.

  1. To maintain visual consistency, we want these rectangles to match the Deck button's dimensions exactly. Switch to the Main.storyboard tab to examine our reference element.

  2. In the Editor or Document Outline, select the Deck Button if it isn't already selected.

  3. If the Size inspector isn't visible on the right panel, click its icon size inspector icon. You may first need to click the Hide or show the Utilities button show hide utilities icon in the top right corner.

  4. We need to extract the size constraints from the Deck button to replicate them programmatically. Under Constraints, examine these critical measurements:

    • Next to the Proportional Height constraint, click Edit to see that the Multiplier is 0.36. Since we constrained the Deck button relative to the view (called superView in our Document Outline), the Deck button is 36% of the view's height.
    • For the width, we constrained the Deck button to itself using the 329:488 Ratio constraint. To implement this ratio programmatically, we'll need to perform calculations to set the width relative to the height.
  5. Switch back to the ViewController.swift tab to implement these calculations.

  6. Set the location and dimensions of player 1's image view with this code:

    cardsImageViews.append(player2CardIV)
    
    player1CardIV.frame = CGRect(x: 0, y: 0, width: view.frame.height * 0.36 * 0.67, height: view.frame.height * 0.36)

    We're using the constraint values from Storyboard to set the CGRect's height to 36% of the view's frame height. For the perfect width, we calculated 329 (width from our 329:488 ratio) divided by 488, yielding 0.67. Since we're multiplying by values less than 1, this creates a frame smaller than the view, with a width smaller than its height—exactly what we need for a card-like aspect ratio.

  7. To visualize our progress, add the player1CardIV to the view as a subview:

    player1CardIV.frame = CGRect(x: 0, y: 0, width: view.frame.height * 0.36 * 0.67, height: view.frame.height * 0.36)
    
    view.addSubview(player1CardIV)
  8. Let's test our implementation so far. In the top left of the window, locate the active scheme. Click where it says Card War and choose iPhone 8 to preview the app on this device.

  9. Click the Run button run icon in the top left corner.

  10. Once the Simulator loads the app, click the Deck button. You'll see a card of identical size appear in the top-left corner of the screen.

    The card appears in this position because we set both the x and y origin coordinates to 0. To reposition the card appropriately, we need to add programmatic positioning constraints.

  11. Return to Xcode to implement proper positioning.

  12. Set the position of player 1's image view using these positioning calculations:

    player1CardIV.frame = CGRect(x: 0, y: 0, width: view.frame.height * 0.36 * 0.67, height: view.frame.height * 0.36)
    player1CardIV.center.x = view.center.x * 0.24
    player1CardIV.center.y = view.center.y * 1.1
    
    view.addSubview(player1CardIV)

    These multiplier values correspond to the factors we used in Storyboard to constrain the Deck button. (You can verify this by checking the multiplier values for the Align Center X and Align Center Y constraints in the Storyboard tab.) Using identical numbers positions player 1's card directly over the Deck button—if we ran the Simulator now, the card wouldn't be visible due to this overlap.

  13. Create player 2's card with matching dimensions and initial positioning:

    player1CardIV.center.y = view.center.y * 1.1
    player2CardIV.frame = player1CardIV.frame
    player2CardIV.center.x = player1CardIV.center.x
    player2CardIV.center.y = player1CardIV.center.y
    
    view.addSubview(player1CardIV)
    view.addSubview(player2CardIV)

    This approach efficiently clones player 1's size and position for player 2. Since we've already performed the dimensional calculations for player 1, we can reference those values directly rather than recalculating—a common optimization technique in iOS development.

  14. For now, let's move player 1's card to its final position (we'll add smooth animations in the next exercise):

    view.addSubview(player2CardIV)
    
    player1CardIV.center.x = view.center.x * 0.57 + cardLayoutDistance * CGFloat(drawNumber)
    player1CardIV.center.y = view.center.y * 0.6
  15. Let's break down this positioning logic:

    • These values were determined through iterative testing to achieve optimal visual placement. Both values position the card slightly below and to the right of the Player 1 Score: label's center, creating a natural visual hierarchy.

    • The center.x calculation is more sophisticated because each card's horizontal position varies based on its drawNumber (a value from 1–26 converted to CGFloat for mathematical operations). This creates a left-to-right progression where subsequent cards stack on top of previous ones, mimicking real card gameplay.

    • The cardLayoutDistance variable equals 1/44 of the view's width—you can verify this in the viewDidAppear function located in the middle of our code.

  16. Position player 2's card with the same horizontal alignment but at the bottom of the screen:

    player1CardIV.center.y = view.center.y * 0.6
    player2CardIV.center.x = player1CardIV.center.x
    player2CardIV.center.y = view.center.y * 1.4
  17. Test the improved positioning by clicking Stop stop icon then Run run icon in the top left.

  18. Click the Deck button to see each player's card appear instantly (without animation) to the right of the deck, positioned near their respective score labels. The visual hierarchy is now much more intuitive and game-like.

Card Sizing Calculations

36%
percent of view height for card height
67
width multiplier ratio (329/488)

Programmatic Constraint Process

1

Analyze Storyboard Constraints

Examine existing Deck button constraints to match dimensions and positioning

2

Calculate Frame Dimensions

Use CGRect with mathematical calculations based on view frame proportions

3

Position Using Center Points

Set center.X and center.Y coordinates relative to view center with multipliers

Revealing the Cards & Updating the Winner's Score

Now we'll implement the exciting reveal mechanism and scoring logic that transforms our static display into a functional game. This section brings together the data model we created earlier with the visual elements we just positioned.

  1. Time for the dramatic reveal! While we'll add smooth flip animations in the next exercise, let's first establish the core functionality by assigning each player their actual drawn card image:

    player2CardIV.center.y = view.center.y * 1.4
    
    player1CardIV.image = player1Card.image
    player2CardIV.image = player2Card.image
  2. Let's test the reveal mechanism. Click Stop stop icon then Run run icon in the top left.

  3. When the app loads, click the Deck button to see the first set of cards revealed instantly. This demonstrates the seamless integration between our data model and View Controller—the card object's image property, initialized in the data model, now displays visually in our UI.

    Note that the score remains at 0 because we haven't implemented the winning logic yet.

  4. Return to Xcode to implement the scoring system that determines round winners.

  5. Add conditional logic to determine which player wins each round:

    player2CardIV.image = player2Card.image
    
    if player1Card.value > player2Card.value { player1Score += 1 }
    if player1Card.value < player2Card.value { player2Score += 1 }

    This implements a simplified version of War where ties result in no score change. When one card has a higher value than the other, the winning player's score increments by one point—clean, straightforward game logic.

  6. Stop stop icon and Run run icon the app again to test the scoring functionality.

  7. Click the Deck button to draw cards. If the cards have different values, you'll see the winning player's score automatically update from 0 to 1. This happens because our player1Score and player2Score variables use the didSet property observer, which monitors changes and updates the associated label outlets accordingly—a elegant example of reactive programming in iOS development.

    If you draw cards with identical values (like two Aces), you won't see a score change unless you restart and try again.

  8. Currently, we can only draw one set of cards because of a state management issue. Return to Xcode to fix this limitation.

  9. Locate this line at the beginning of the drawCards function:

    if drawingCards { return }

    If the drawingCards boolean is true, the function assumes a draw operation is still in progress and prevents further execution. Since we haven't updated this variable, users can't continue playing.

  10. At the bottom of the drawCards function, reset the state to allow continued gameplay:

    if player1Card.value < player2Card.value { player2Score += 1 }
    
    self.drawingCards = false
  11. Stop stop icon and Run run icon the app to test continuous gameplay.

  12. Click the Deck button multiple times, keeping track of your clicks. The app now properly maintains score across multiple rounds!

  13. Continue clicking until each player has drawn all 26 cards to test the end-game scenario.

    This will trigger an EXC_BAD_INSTRUCTION error on the line attempting to removeLast from the empty shuffledDeck array.

  14. Implement proper end-game handling with this conditional logic:

    if player1Card.value < player2Card.value { player2Score += 1 }
    
    if drawNumber == 26 {
       deckButton.imageView?.image = UIImage(named: "Game-Over")
       gameOver = true
    }
    self.drawingCards = false

    The deckButton outlet allows us to change the button's image from the standard card back to a game-over image (located in the Images folder). Setting gameOver to true triggers the end-game state, preparing for restart functionality.

  15. To understand the restart flow, examine this code near the top of the drawCards function:

    if gameOver {
       restartGame()
       gameOver = false
       return
    }

    When users click the Deck button after the 26th draw, this code executes the restart sequence, resets the game state, and exits the function—providing seamless game flow.

Data Model Integration

When the card object was initialized in the data model, it fetched its own image. The data model interacts with the View Controller to display information visually in the UI.

Score Update Implementation

1

Compare Card Values

Use if statements to determine which player has the higher value card

2

Increment Winner Score

Add 1 point to the winning player's score variable using += operator

3

Update UI Automatically

didSet property observer watches score variables and updates label outlets accordingly

Defining What Happens When the Game is Restarted

The final piece of our game mechanics involves implementing the restart functionality. This system needs to handle both automatic restarts (when the game ends) and manual restarts (when users click the restart button). Proper restart implementation ensures users can play multiple games without encountering state management issues.

  1. Let's connect the red restart button card war restart button in our UI to the restartGame function we're about to implement. Clean up the restartButton function to call our restart logic efficiently:

    @IBAction func restartButton(_ sender: Any) { restartGame() }

Game Restart Requirements

0/4

Key Takeaways

1UIImageView objects serve as containers for displaying card images dynamically during gameplay, with initial card back images creating suspense before reveal
2Programmatic constraints using CGRect and center positioning provide precise control over UI element sizing and placement without relying on Storyboard
3Card sizing calculations use proportional multipliers (36% height, 0.67 width ratio) to maintain consistent dimensions relative to existing UI elements
4Score management relies on didSet property observers that automatically update UI labels when player score variables change during gameplay
5Game state management requires careful handling of boolean flags like drawingCards and gameOver to prevent user interface conflicts and crashes
6Array-based card tracking through cardsImageViews enables proper cleanup and memory management when games restart
7Data model integration allows card objects to provide their own images, demonstrating separation of concerns between data and presentation layers
8Semi-colon usage in Swift provides coding style flexibility, allowing multiple statements per line while maintaining code readability preferences

RELATED ARTICLES