Learn Courses My Dashboard

Display problem following lesson 9 - Match Game

Hello everyone !
First of all, sorry for my English ! I try to improve it :grin:
After having followed all the videos concerning the development of the Match Game, a problem occurs in my application … To explain my situation, when I select a first card in the first row at the top of the collection view, and then a second in the last row at the bottom of the collection view, a display bug occurs. My cards are moving to the left…

screenshot of the problem :

I’m sharing my code with you so that maybe it helps you explain the source of the problem to me. Note that I allowed myself to reorganize it by respecting the MVC model (I am new to MVC so there may be some inconsistencies with it).

NOTE : With breakpoints, I see that after giving the index of the first card selected to “firstFlippedCardIndex”, this variable don’t save the index. I have this message : “firstFlippedCardIndex IndexPath? Failed to get the ‘some’ field from optional ‘firstFlippedCardIndex’ (0x0)”
Do you know what this is ?



Model MatchGame.swift

import Foundation

/// This class represents a match game
class MatchGame {
    /// This array stores generated cards
    var generatedCardsArray = [Card]()
    
    /// This integer saves the numbers of errors that the player will do
    var numberOfErrors = 0
    
    /// This method randomly generates pairs of cards
    func getCards() {
        var generatedNumbersArray = [Int]()
        generatedCardsArray = []
        
        // As long as the 16 cards are not defined
        while generatedNumbersArray.count < 8 {
            
            // Randomly declare a pair of cards
            let randomNumber = Int.random(in: 1...13)
            
            // Check if the number has not already been generated previously
            if !generatedNumbersArray.contains(randomNumber) {
                
                // Log the number generated
                print("Generating a random number : \(randomNumber)")
                
                // Create the first card object of the pair
                let cardOne = Card()
                cardOne.imageName = "card\(randomNumber)"
                
                // Create the second card object of the pair
                let cardTwo = Card()
                cardTwo.imageName = "card\(randomNumber)"
                
                // Add the pair in the array
                generatedCardsArray.append(cardOne)
                generatedCardsArray.append(cardTwo)
                
                // Save the generated number so as not to get it a second time
                generatedNumbersArray.append(randomNumber)
            }
        }
        
        // Randomize the cards in the array
        generatedCardsArray.shuffle()
        
        // Log the number of cards created
        print("Number of cards created : \(generatedCardsArray.count)")
    }
    
    
    /// This method check if the selected card can be flip
    ///
    /// - parameter cardIndex : index of the card selected in the array of generated cards
    ///
    /// - returns : a bool where it is true if the card can be flip or otherwise false
    func checkForFlip(cardIndex: IndexPath) -> Bool {
        // Get the card selected in the array
        let card = generatedCardsArray[cardIndex.row]
        
        // Check if the card is not already flipped or matched, to can flip it
        if !card.isFlipped && !card.isMatched {
            card.isFlipped = true
            return true
        }
        
        return false
    }
    
    
    /// This method check if the two cards selected matches or not
    ///
    /// - parameter firstFlippedCardIndex : index of the first card selected in the array of generated cards
    /// - parameter secondFlippedCardIndex : index of the second card selected in the array of generated cards
    ///
    /// - returns : a bool where it is true if the two cards match or otherwise false
    func checkForMatches(_ firstFlippedCardIndex: IndexPath, _ secondFlippedCardIndex: IndexPath) -> Bool {
        let cardOne = generatedCardsArray[firstFlippedCardIndex.row]
        let cardTwo = generatedCardsArray[secondFlippedCardIndex.row]
        
        if cardOne.imageName == cardTwo.imageName {
            cardOne.isMatched = true
            cardTwo.isMatched = true
            
            return true
        } else {
            numberOfErrors += 1
            
            // Set the statuses of the cards
            cardOne.isFlipped = false
            cardTwo.isFlipped = false
            
            return false
        }
    }
    
    
    /// This method check if all cards are matched
    ///
    /// - returns : a bool where it is true if all cards are matched or otherwise false
    func checkGameEnded() -> Bool {
        // Determine if there are any cards unmatched
        for card in generatedCardsArray {
            if card.isMatched == false {
                return false
            }
        }
        
        return true
    }
}


/// This class represents a card
class Card {
    
    /// This string is the image name of the card
    var imageName = ""
    
    /// This bool represents if the card is flipped or not
    var isFlipped = false
    
    /// This bool represents if the card is matched or not
    var isMatched = false
}

Controller MatchGameViewController.swift

import UIKit

/// This class is the main view controller of the match game
class MatchGameViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

    /// This button is used to start a game
    @IBOutlet weak var startGameButton: UIButton!
    
    /// This collection view display the different cards of a game
    @IBOutlet weak var cardsCollectionView: UICollectionView!
    
    /// This instance contains the different games one by one
    var game: MatchGame!
    
    /// This property saves the index of the first card selected
    var firstFlippedCardIndex: IndexPath?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        cardsCollectionView.isHidden = true
    }
    
    /// This action is performed when the startGameButton is pressed
    @IBAction func startGame(_ sender: Any) {
        
        // Create a new game
        game = MatchGame()
        
        // Get new cards for the new game
        game.getCards()
        
        // Play the shuffle sound
        SoundManager.playSound(.shuffle)
        
        // Update the view for the start of the game
        startGameButton.isHidden = true
        cardsCollectionView.isHidden = false
        // Set the view controller as the datasource and delegate of the collection view
        cardsCollectionView.delegate = self
        cardsCollectionView.dataSource = self
        cardsCollectionView.reloadData()
    }
    
    // MARK: - UICollectionView protocol methods
    
    /// This method tells the collection view the number of cards to display
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        
        // Return the number of cards
        return game.generatedCardsArray.count
    }
    
    /// This method configure each of the cards in the collection view
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        // Get the CardCollectionViewCell to configure it
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cardCell", for: indexPath) as! CardCollectionViewCell
        
        // Get the card that the collection view is trying to display
        let card = game.generatedCardsArray[indexPath.row]
        
        // Set the card for the cell
        cell.setCard(card)
        
        // Return the cell to display
        return cell
    }
    
    /// This function is executed when a card is selected from the collection view
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        // Get the cell that the patient selected
        let cell = collectionView.cellForItem(at: indexPath) as! CardCollectionViewCell
        
        // Check if the selected card can be flipped
        if game.checkForFlip(cardIndex: indexPath) {
            
            // Play the flip sound
            SoundManager.playSound(.flip)
            
            // Flip the card
            cell.flip()
            
            // Check if it is the first card that's flipped over or not
            if firstFlippedCardIndex == nil {
                
                //Save the indexPath of the first card flipped
                firstFlippedCardIndex = indexPath
                
            } else {
                
                // Get the cells for the two cards that were revealed
                let cardOneCell = cardsCollectionView.cellForItem(at: firstFlippedCardIndex!) as? CardCollectionViewCell
                let cardTwoCell = cardsCollectionView.cellForItem(at: indexPath) as? CardCollectionViewCell
                
                // Check if the two cards match
                if game.checkForMatches(firstFlippedCardIndex!, indexPath) {
                    
                    // Play the match sound
                    SoundManager.playSound(.match)
                    
                    // Remove the cards from the grid
                    cardOneCell?.removeCardMatched()
                    cardTwoCell?.removeCardMatched()
                    
                    // Check if there are any cards left unmatched
                    if game.checkGameEnded() {
                        
                        // Show an alert to notify the player that the game is over
                        showAlert(title: "Jeu terminé !", message: "Nombre d'erreurs : \(game.numberOfErrors)\nTemps réalisé : \("x")")
                        cardsCollectionView.isHidden = true
                        startGameButton.isHidden = false
                    }
                } else {
                    // Play the no match sound
                    SoundManager.playSound(.noMatch)
                    
                    // Flip both cards back
                    cardOneCell?.flipBack()
                    cardTwoCell?.flipBack()
                    
                }
                
                // Tell the collection view to reload the cell of the first card if it is nil
                if cardOneCell == nil {
                    cardsCollectionView.reloadItems(at: [firstFlippedCardIndex!])
                }
                
                // Reset the card one index saved
                firstFlippedCardIndex = nil
            }
        }
    }
    
    // MARK: - Game Logic Methods
    
    /// This method show an alert with title and message passed in parameters
    ///
    /// - parameter title : title of the alert
    /// - parameter message : message of the alert
    func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (alert: UIAlertAction!) in
            // Save the game in database
        }))
        self.present(alert, animated: true)
    }
    
    /// This action is performed when the exit button is pressed
    @IBAction func exitMatchGame(_ sender: Any) {
        performSegue(withIdentifier: "exitMatchGame", sender: self)
    }
    
}

Controller CardCollectionViewCell.swift

import UIKit

/// This class is the collection view cell representing the cards of the game
class CardCollectionViewCell: UICollectionViewCell {
    
    /// This image view is the front of the card
    @IBOutlet weak var frontImageView: UIImageView!
    
    /// This image view is the back of the card
    @IBOutlet weak var backImageView: UIImageView!
    
    /// This instance contains the card of the cell
    var card: Card?
    
    
    /// This method sets the card to the cell
    ///
    /// - parameter card : The card to set it to the cell
    func setCard(_ card: Card) {
        
        // Keep track of the card that gets passed in
        self.card = card
        
        // Determine if the card has been matched or not
        if card.isMatched {
            
            // Make sure the imageViews are invisibles
            backImageView.alpha = 0
            frontImageView.alpha = 0
            return
            
        } else {
            
            // Make sure the imageViews are visibles
            backImageView.alpha = 1
            frontImageView.alpha = 1
        }
        
        // Set the image of the card to front image view
        frontImageView.image = UIImage(named: card.imageName)
        
        // check if the card is in a flipped up state or flipped down state
        if card.isFlipped {
            
            //Make sure the frontImageView is on top
            UIView.transition(from: backImageView, to: frontImageView, duration: 0, options: [.transitionFlipFromLeft, .showHideTransitionViews], completion: nil)
            
        } else {
            
            //Make sure the backImageView is on top
            UIView.transition(from: frontImageView, to: backImageView, duration: 0, options: [.transitionFlipFromLeft, .showHideTransitionViews], completion: nil)
        }
    }
    
    /// This method flips the card
    func flip() {
        
        // Play the flip sound
        SoundManager.playSound(.flip)
        
        UIView.transition(from: backImageView, to: frontImageView, duration: 0.3, options: [.transitionFlipFromLeft, .showHideTransitionViews], completion: nil)
    }
    
    /// This method flips back the card
    func flipBack() {
        
        //A DispatchTime to give the player time to look at the cards
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
            UIView.transition(from: self.frontImageView, to: self.backImageView, duration: 0.3, options: [.transitionFlipFromRight, .showHideTransitionViews], completion: nil)
        }
    }
    
    /// This method hide the card when it is matched
    func removeCardMatched() {
        
        // Hide both imageViews from being visible
        backImageView.alpha = 0
        
        UIView.animate(withDuration: 0.3, delay: 0.5, options: .curveEaseOut, animations: {
            self.frontImageView.alpha = 0
        }, completion: nil)
    }
}

Can you help me ?

I tried to see what was really going on with alpha = 0.5 in remove ().
I found a first pair, which therefore has alpha = 0.5 (screenshot 1) to see the problem. We then notice that when I select a third card on the first row (screenshot 2) and that I look for a fourth at the bottom of the view collection, the third card disappears (screenshot 3). We see that the two cards found at the beginning are adjacent when they were not. And when I scroll up, the third card reappears (same that screenshot 1).
It’s like she’s actually been deleted when she’s not even matched.
The problem also occurs for the first pair to be found. I did this with a second pair so we could see the problem well.

I see it only does this with the first card selected … Could this be the problem with firstFlippedCardIndex that I tall in my first post?

screenshot 1

screenshot 2

screenshot 3

I might be easier to compare your code against the solution you can find in the Introduction section under Materials for this course

I don’t see where this page is… do you have a link that would redirect me to this?

@DydouBrr

Are you a member of CWC+?

@Chris_Parker No. I’m just following your videos on YouTube. I had explained my problem in comment under the video and I was advised to ask my question here. :sweat_smile:

OK in that case you won’t have access to the completed code to make a comparison to yours.

BTW, just so there is no confusion going forward, I’m not Chris Ching who is the creator of the content you are watching. I’m a moderator on this community who just happens to have the same first name.

Oh ok! So I’ll try to find the solution for myself! :blush: Thanks anyway :wink:

The best advice that I can give is to go over the videos again to be sure that you have not missed a key element.