Topics Covered in This iOS Development Tutorial:
Declaring variables with proper scope management, implementing property observers with didSet for reactive UI updates, and building the foundation for card drawing functionality in a complete iOS game
Exercise Overview
In this comprehensive exercise, we transition from our data model architecture to the View Controller implementation—the critical bridge between your app's logic and user interface. You'll master the art of defining variables and constants that enable your View Controller to dynamically represent cards from our deck, while learning to create variables that trigger smooth animated transitions whenever their values change. This hands-on approach demonstrates how professional iOS developers structure View Controller code to seamlessly interact with both Storyboard UI elements and underlying data models, establishing patterns you'll use throughout your career in mobile development.
Card War Development Progress
Data Model Creation
Built Card and Deck classes with proper data structure
View Controller Variables
Adding variables and constants for UI representation
Card Drawing Logic
Implementing the core game functionality
Getting Started
Launch Xcode if it isn't already running. Ensure you're working with the latest version for optimal performance and access to current iOS development features.
If you completed the previous exercise, Card War.xcodeproj should remain open in your workspace. If you closed it, navigate back and reopen the project file.
We strongly recommend completing the previous exercises (5A–5B) before proceeding, as this tutorial builds directly on those foundational concepts. If you did not complete the previous exercises, follow these steps to catch up:
- Navigate to File > Open in the menu bar.
- Browse to Desktop > Class Files > yourname-iOS App Dev 1 Class > Card War Ready for View Controller Variables and double-click Card War.xcodeproj to launch the prepared project.
Pre-Exercise Requirements
Essential foundation for View Controller implementation
Main project file containing all previous work
Reference materials for cross-component integration
Primary workspace for this exercise
Declaring Variables
With our data model architecture complete, we now focus on the View Controller—the orchestrator of your app's user experience. Here, we'll establish the variables that manage UI state changes as gameplay progresses, following iOS development best practices for memory management and performance optimization.
Professional iOS development requires understanding how different components interact. To visualize these relationships effectively, ensure you have Main.storyboard and Data Model.swift open in separate Editor tabs. This multi-pane approach mirrors real-world development workflows where developers constantly reference interconnected files. Use Cmd–T to open new tabs as needed.
Create another tab with Cmd–T, then select ViewController.swift from the Project navigator. This file will contain the core logic driving your app's interactive behavior.
We'll begin by instantiating our custom Deck class within the View Controller. Below the three @IBOutlets and before the @IBAction functions, add this essential variable:
@IBOutlet weak var player2ScoreLabel: UILabel! var deck = Deck() @IBAction func drawCards(_ sender: Any) {This creates a direct connection between your View Controller and the data model, enabling seamless card management throughout the game lifecycle.
Modern iOS apps require efficient memory management, especially when dealing with dynamically created UI elements. Each card draw generates two UIImageView instances for visual representation. Add this array to manage these views effectively:
var deck = Deck() var cardsImageViews = [UIImageView]() @IBAction func drawCards(_ sender: Any) {This array provides centralized storage for all UIImageViews created during gameplay, enabling proper cleanup when games restart. Without this management system, new game cards would overlay previous cards, creating visual chaos and memory leaks that could crash your app—a critical consideration in professional iOS development.
Game state tracking requires precise monitoring of user interactions. Implement a counter for card draw events:
var cardsImageViews = [UIImageView]() var drawNumber = 0 @IBAction func drawCards(_ sender: Any) {With 52 cards distributed between two players, drawNumber reaches a maximum of 26. Starting at 0 ensures your app recognizes the initial state where no cards have been drawn. This variable will increment with each draw, providing essential game flow control.
Professional iOS apps must adapt to various device sizes and orientations. Create a responsive layout system for card positioning:
var drawNumber = 0 var cardLayoutDistance: CGFloat! @IBAction func drawCards(_ sender: Any) {This CGFloat variable ensures consistent, proportional spacing between cards across different iOS devices, from iPhone SE to iPad Pro. The calculated spacing factor maintains visual harmony regardless of screen dimensions.
As an implicitly unwrapped optional, this variable requires initialization when the view appears. Implement the viewDidAppear method for precise timing:
var cardLayoutDistance: CGFloat! override func viewDidAppear(_ animated: Bool) { cardLayoutDistance = view.frame.width / 44 } @IBAction func drawCards(_ sender: Any) {Understanding the calculation logic enhances your development skills:
The viewDidAppear method ensures accurate view dimensions are available before calculation. Attempting this in viewDidLoad could result in incorrect measurements since Auto Layout constraints haven't fully resolved.
The division factor of 44 represents optimal visual spacing derived through iterative testing across multiple device sizes. This approach demonstrates how professional developers balance mathematical precision with visual design principles. As shown in the end-game screenshot below, this calculation ensures all cards remain visible with appropriate margins.

Essential View Controller Variables
Deck Instance
Single deck variable manages all card data and shuffling operations for the entire game session.
Cards Array Management
UIImageView array prevents memory overflow by tracking and removing old card displays during game restart.
Draw Counter
Tracks drawing progress with maximum value of 26 for a standard 52-card deck split between players.
Layout Distance
CGFloat calculated as view width divided by 44 for optimal card spacing across different screen sizes.
The cardsImageViews array prevents memory crashes by storing references to all UIImageViews so they can be properly removed during game restart, preventing card overlap.
Responding to Changes in a Variable's Value Using the DidSet Property Observer
Property observers represent one of Swift's most powerful features, enabling reactive programming patterns that keep your UI synchronized with data changes. This approach eliminates manual UI updates and reduces bugs common in traditional imperative programming.
Implement score tracking variables for both players with clean initialization:
var cardsImageViews = [UIImageView]() var player1Score = 0 var player2Score = 0 var drawNumber = 0Property observers enable automatic UI updates whenever score values change—a cornerstone of modern iOS development. Add the didSet observer to player1Score:
var player1Score = 0 { didSet { } } var player2Score = 0 {The didSet property observer executes immediately after a variable's value changes, providing a clean separation between data updates and UI responses. This pattern ensures your interface remains synchronized with underlying game state without manual intervention.
iOS provides sophisticated animation APIs for engaging user experiences. Begin implementing the transition animation by typing UIView.transition within the didSet block and selecting the transition(with: option from Xcode's intelligent code completion:
var player1Score = 0 { didSet { UIView.transition(with: UIView, duration: TimeInterval, options: UIViewAnimationOptions, animations: (() -> Void)?, completion: ((Bool) -> Void)?) } }This UIView class method orchestrates smooth animated transitions, providing professional polish that distinguishes quality apps from basic implementations.
Specify the target for animation by replacing the UIView placeholder with our score label. Replace the highlighted placeholder and press Tab to advance:
didSet { UIView.transition(with: player1ScoreLabel, duration: TimeInterval, options: UIViewAnimationOptions, animations: (() -> Void)?, completion: ((Bool) -> Void)?) }The player1ScoreLabel IBOutlet connects to the numeric display in your Storyboard, creating the bridge between code logic and visual presentation.
Configure animation timing and style for optimal user experience. Set duration and transition type by filling the next placeholders:
didSet { UIView.transition(with: player1ScoreLabel, duration: 0.3, options: .transitionCurlUp, animations: (() -> Void)?, completion: ((Bool) -> Void)?) }The 0.3-second duration provides noticeable feedback without slowing gameplay, while the curl-up transition adds visual flair that enhances the card game aesthetic.
Define the actual content change within the animations closure. This block executes during the transition, updating the label's text:
didSet { UIView.transition(with: player1ScoreLabel, duration: 0.3, options:.transitionCurlUp, animations: { self.player1ScoreLabel.text = "\(self.player1Score)" }, completion: ((Bool) -> Void)?) }The self keyword is required within closures to explicitly reference the View Controller's properties. This Swift safety feature prevents retain cycles and clarifies scope, essential for memory management in professional iOS development.
Complete the transition method by specifying the completion handler. Since no post-animation actions are needed, set completion to nil:
didSet { UIView.transition(with: player1ScoreLabel, duration: 0.3, options:.transitionCurlUp, animations: { self.player1ScoreLabel.text = "\(self.player1Score)" }, completion: nil) }Copy the complete player1Score implementation for reuse with player2. Select the entire variable declaration and press Cmd–C:
var player1Score = 0 { didSet { UIView.transition(with: player1ScoreLabel, duration: 0.3, options:.transitionCurlUp, animations: { self.player1ScoreLabel.text = "\(self.player1Score)" }, completion: nil) } }Replace the simple var player2Score = 0 line by selecting it and pasting with Cmd–V.
Adapt the copied code for player2 by updating all references in the duplicated variable:
var player1Score = 0 { didSet { UIView.transition(with: player1ScoreLabel, duration: 0.3, options:.transitionCurlUp, animations: { self.player1ScoreLabel.text = "\(self.player1Score)" }, completion: nil) } } var player2Score = 0 { didSet { UIView.transition(with: player2ScoreLabel, duration: 0.3, options:.transitionCurlUp, animations: { self.player2ScoreLabel.text = "\(self.player2Score)" }, completion: nil) } }
Implementing Animated Score Updates
Add didSet Observer
Property observer automatically detects when player score variables change and triggers animation code
Configure UIView Transition
Set transition target to score label with 0.3 second duration and curl-up animation effect
Update Label Text
Animation block updates label text with new score value using string interpolation and self reference
Handle Completion
Set completion parameter to nil since no additional actions needed after animation finishes
The self keyword is required when accessing View Controller properties inside UIView transition closures, as they run outside the main thread context.
Starting with the DrawingCards Function
The card drawing mechanism forms the heart of our War game implementation. This function orchestrates the complex interaction between user input, data manipulation, and visual feedback—demonstrating how professional iOS apps handle real-time user interactions with robust state management.
Implement state management variables to prevent race conditions and handle user interactions gracefully. Add these boolean flags between cardLayoutDistance and viewDidAppear:
var drawNumber = 0 var cardLayoutDistance: CGFloat! var drawingCards = false override func viewDidAppear(_ animated: Bool) { cardLayoutDistance = view.frame.width / 44 }The drawingCards flag implements a simple but effective locking mechanism. When users tap the draw button, animations and score updates don't occur instantaneously. During the 0.3-second animation duration, impatient users might tap repeatedly, causing overlapping animations and inconsistent game state. This boolean prevents such issues by blocking additional draws until current operations complete.
Add game completion tracking with another boolean state variable:
var drawingCards = false var gameOver = falseThis flag transforms the card back button's behavior when the deck is exhausted, changing from "draw cards" to "restart game" functionality. Later exercises will demonstrate how this variable triggers visual changes to the button image, providing clear user feedback about available actions:

Begin implementing the drawCards function with essential guard clauses. Add this protective code to prevent unwanted interactions:
@IBAction func drawCards(_ sender: Any) { if drawingCards { return } }This guard clause immediately exits the function if cards are currently being drawn, preventing the concurrency issues that plague poorly designed interactive apps. The early return pattern keeps code clean and readable.
Handle game completion state with appropriate restart logic. Add comprehensive game-over handling:
@IBAction func drawCards(_ sender: Any) { if drawingCards { return } if gameOver { restartGame() gameOver = false return } }When the game ends, tapping the button triggers a restart sequence rather than attempting to draw from an empty deck. This elegant state-based approach eliminates conditional logic scattered throughout the codebase.
Resolve the compiler error by creating the restartGame function stub. Add this placeholder beneath your other functions but within the ViewController class:
@IBAction func restartButton(_ sender: Any) { } func restartGame() { } }This function will be implemented in subsequent exercises, but creating the signature now allows your current code to compile successfully.
Implement the core card drawing logic with proper state management. Add these essential operations after the guard clauses:
if gameOver { restartGame() gameOver = false return } drawingCards = true drawNumber += 1This dual assignment pattern first sets the lock to prevent additional draws, then increments the draw counter. Starting from 0, drawNumber tracks how many card pairs have been drawn, reaching 26 when the 52-card deck is exhausted.
Extract cards from the shuffled deck using Swift's array manipulation methods. Add these card assignment statements:
drawNumber += 1 let player1Card = deck.shuffledDeck.removeLast() let player2Card = deck.shuffledDeck.removeLast()These constants capture the two topmost cards from your shuffled deck using Apple's removeLast() method, which both retrieves and removes elements atomically. The Deck class's shuffledDeck property maintains a randomized copy of the original deck, ensuring fair gameplay while preserving the original deck structure for future games. Using constants rather than variables reflects these cards' immutable nature within each draw cycle—a best practice that prevents accidental modification and clarifies intent.
Save your progress with Cmd–S and keep Xcode open. The foundation you've built here establishes the essential patterns for iOS game development, combining state management, memory efficiency, and user experience considerations that define professional-quality mobile applications.
Game State Management Variables
Drawing Cards Boolean
Prevents impatient users from tapping the draw button multiple times while card animations are still in progress.
Game Over Boolean
Controls game state and triggers restart functionality when deck is exhausted, changing button visual indicator.
DrawCards Function Logic Flow
Check Drawing State
Return immediately if cards are currently being drawn to prevent multiple simultaneous operations
Handle Game Over
Restart game and reset gameOver flag if deck is exhausted, then exit function
Set Drawing State
Set drawingCards to true and increment drawNumber to track current round
Assign Player Cards
Remove last two cards from shuffled deck and assign one to each player using removeLast method
The drawingCards boolean prevents UI conflicts by blocking new card draws during the 0.3-second animation transitions, ensuring smooth gameplay.