Hello everyone !
First of all, sorry for my English ! I try to improve it
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 ?