- Swift 2 By Example
- Giordano Scalzo
- 1461字
- 2021-07-16 12:45:46
Implementing a deck of cards
So far, we have implemented a pretty generic app that lays out views inside a bigger view. Let's proceed to implement the foundation of the game: a deck of cards.
What we are expecting
Before implementing the classes for a deck of cards, we must define the behavior we are expecting, whereby we implement the calls in MemoryViewController
, assuming that the Deck
object already exists. First of all, we change the type in the definition of the property:
private var deck: Deck!
Then, we change the implementation of the start()
function:
private func start() { deck = createDeck(numCardsNeededDifficulty(difficulty)) collectionView.reloadData() } private func createDeck(numCards: Int) -> Deck { let fullDeck = Deck.full().shuffled() let halfDeck = fullDeck.deckOfNumberOfCards(numCards/2) return (halfDeck + halfDeck).shuffled() }
We are saying that we want a deck to be able to return a shuffled version of itself and which can return a deck of a selected numbers of its cards. Also, it can be created using the plus operator (+
) to join two decks. This is a lot of information, but it should help you learn a lot regarding structs
.
The card entity
There hasn't been anything regarding the entities inside Deck
so far, but we can assume that it is a Card
struct and that it uses plain enumerations. A Suit
and Rank
parameter define a card, so we can write this code in a new file called Deck.swift
:
enum Suit: CustomStringConvertible { case Spades, Hearts, Diamonds, Clubs var description: String { switch self { case .Spades: return "spades" case .Hearts: return "hearts" case .Diamonds: return "diamonds" case .Clubs: return "clubs" } } } enum Rank: Int, CustomStringConvertible { case Ace = 1 case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten case Jack, Queen, King var description: String { switch self { case .Ace: return "ace" case .Jack: return "jack" case .Queen: return "queen" case .King: return "king" default: return String(self.rawValue) } } }
Note that we have used an integer
as a type in Rank
but not in Suit
. That's because we want the possibility of creating a Rank
from an integer, its raw value, but not for Suit
. This will soon become clearer.
We have implemented the CustomStringConvertible
protocol, called Printable
in Swift 1.x, in order to be able to print the enumeration. The Card
parameter is nothing more than a pair of Rank
and Suit
cases:
struct Card: CustomStringConvertible, Equatable { private let rank: Rank private let suit: Suit var description: String { return "\(rank.description)_of_\(suit.description)" } } func ==(card1: Card, card2: Card) -> Bool { return card1.rank == card2.rank && card1.suit == card2.suit }
Also, for Card
, we have implemented the CustomStringConvertible
protocol, basically joining the description of its Rank
and Suit
cases. We have also implemented the Equatable
protocol to be able to check whether two cards are of the same value.
Crafting the deck
Now we can implement the constructor of a full deck, iterating through all the values of the Rank
and Suit
enumerations:
struct Deck { private var cards = [Card]() static func full() -> Deck { var deck = Deck() for i in Rank.Ace.rawValue...Rank.King.rawValue { for suit in [Suit.Spades, .Hearts,.Clubs, .Diamonds] { let card = Card(rank: Rank(rawValue: i)!, suit: suit) deck.cards.append(card) } } return deck } }
Shuffling the deck
The next function we will implement is shuffled()
:
// Fisher-Yates (fast and uniform) shuffle func shuffled() -> Deck { var list = cards for i in 0..<(list.count - 1) { let j = Int(arc4random_uniform(UInt32(list.count - i))) + i if i!= j { swap(&list[i], &list[j]) } } return Deck(cards: list) }
The usual way to shuffle a deck of cards in a computer program is to use the Fisher-Yates algorithm. Starting from the first card, we iterate until the very end, each time swapping the current card with a random card in the set with an index higher than the current one. A complete explanation of this can be found on Wikipedia at http://en.wikipedia.org/wiki/Fisher–Yates_shuffle.
If you look carefully at the swap()
function, you will see an ampersand (&
) symbol before the parameters. This means that the parameters are input and that they can be changed inside functions. We can consider input parameters as shared variables between the caller and the called.
Also, the swap()
function needs two different variables to swap; it isn't possible to swap a variable with itself, so before swapping, we check whether the indices are different.
Finishing the deck
We are almost done with the expected behavior of Deck
; we just need to add the creation of a subset of Deck
:
func deckOfNumberOfCards(num: Int) -> Deck { return Deck(cards: Array(cards[0..<num])) }
Note that using the notation for the [..<]
range, the upper bound is not included in the range, whereas using [..]
, the upper bound is included. We can create this by exploiting the splicing feature of the Swift Array. Using this trick, we create the sum
operator:
func +(deck1: Deck, deck2: Deck) -> Deck { return Deck(cards: deck1.cards + deck2.cards) }
Note that this function must be defined outside the Deck
struct.
The last function left is the count
property, which we implement using a computed property:
var count: Int { get { return cards.count } }
Before moving on to implementing the remainder of the game, we want to check whether everything works fine, so we add a log after creating Deck
, like this:
init(difficulty: Difficulty) { self.difficulty = difficulty self.deck = Deck() super.init(nibName: nil, bundle: nil) self.deck = createDeck(numCardsNeededDifficulty(difficulty)) for i in 0..<deck.count { print("The card at index [\(i)] is [\(deck[i].description)]") } }
Unfortunately, the compiler complains that it doesn't know how to retrieve the element at a specified index.
For the purpose of mimicking the accessor of an array, Swift provides a special computed property to add to the definition of our struct subscript. Implementing the subscript just involves forwarding the request to private property cards:
subscript(index: Int) -> Card { get { return cards[index] } }
Now the app gets compiled. If we run it, we get a console output like this:
The card at index [0] is [8_of_clubs] The card at index [1] is [ace_of_spades] The card at index [2] is [ace_of_clubs] The card at index [3] is [ace_of_hearts] The card at index [4] is [9_of_hearts] The card at index [5] is [ace_of_hearts] The card at index [6] is [queen_of_clubs] The card at index [7] is [ace_of_clubs] The card at index [8] is [ace_of_spades] The card at index [9] is [queen_of_clubs] The card at index [10] is [9_of_hearts] The card at index [11] is [8_of_clubs]
Note
The source code for this block can be downloaded from https://github.com/gscalzo/Swift2ByExample/tree/2_Memory_3_Cards_Foundation.
Put the cards on the table
Finally, let's add the card images and implement the entire game.
Adding the assets
Now that everything works, let's create a nice UI again.
First of all, let's import all the assets in the project. There are plenty of free card assets on the Internet, but if you are lazy, I've prepared a complete deck of images ready for this game for you, and you can download it from https://github.com/gscalzo/Swift2ByExample/raw/2_Memory_4_Complete/Memory/Assets/CardImages.zip.
The archive contains an image for the back of the cards and another image for the front. To include them in the app, select the image assets file from the project structure view, as shown in this screenshot:
After selecting the catalog, the images can be dragged into Xcode, as shown in the following screenshot:
In this operation, you must pay attention and ensure that you move all the images from 1x to 2x as shown in this screenshot. Otherwise, when you run the app, you will see them pixelate.
The CardCell structure
Let's go ahead and implement our CardCell
structure. Again, we pretend that we already have the class, so we register that class during the setup of Collection View:
func setup() { // collectionView.registerClass(CardCell.self, forCellWithReuseIdentifier: "cardCell") // }
Then, we implement the rendering of the class when the data source protocol asks for a cell given an index:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cardCell", forIndexPath: indexPath) as! CardCell let card = deck[indexPath.row] cell.renderCardName(card.description, backImageName: "back") return cell }
We are trying to push as much presentation code as we can into the new class in order to decouple the responsibilities of Cell
and controller
, which hold the model.
So, let's implement a new class called CardCell
, which inherits from UICollectionViewCell
, so don't forget to select that class in the Xcode wizard.
CardCell
contains only UIImageView
to present the card images and two properties to hold the names of the front and back images:
class CardCell: UICollectionViewCell { private let frontImageView: UIImageView! private var cardImageName: String! private var backImageName: String! override init(frame: CGRect) { frontImageView = UIImageView(frame: CGRect( origin: CGPointZero, size: frame.size)) super.init(frame: frame) contentView.addSubview(frontImageView) contentView.backgroundColor = UIColor.clearColor() } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func renderCardName(cardImageName: String, backImageName: String){ self.cardImageName = cardImageName self.backImageName = backImageName frontImageView.image = UIImage(named: self.backImageName) } }
If you run the app now, you should see some nice cards face down.
Handling touches
Now, let's get the cards face up!
This code is part of the UICollectionViewDelegate
protocol, so it must be implemented inside the MemoryViewController
class:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { let cell = collectionView.cellForItemAtIndexPath(indexPath) as! CardCell cell.upturn() }
This code is pretty clear, and now we only need to implement the upturn()
function inside CardCell
:
func upturn() { UIView.transitionWithView(contentView, duration: 1, options: .TransitionFlipFromRight, animations: { self.frontImageView.image =UIImage(named: self.cardImageName) }, completion: nil) }
By leveraging a handy function inside the UIView
class, we have created a nice transition from the back image to the front image, simulating the flip of a card.
To complete the functions required to manage the card from a visual point of view, we implement the downturn()
function in a similar way:
func downturn() { UIView.transitionWithView(contentView, duration: 1, options: .TransitionFlipFromLeft, animations: { self.frontImageView.image = UIImage(named: self.backImageName) },completion: nil) }
To test all the functions, we turn down the card for 2 seconds after we have turned it up. To run a delayed function, we use the dispatch_after
function, but to remove the boilerplate call, we wrap it in a smaller common function, added as an extension of UIViewController
:
extension UIViewController { func execAfter(delay: Double, block: () -> Void) { dispatch_after( dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC)) ), dispatch_get_main_queue(), block) } }
So, after having the card turned up, we turn it down using this newly implemented function:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { //... cell.upturn() execAfter(2) { cell.downturn() } }
By running the app, we now see the cards turning up and down with a smooth and nice animation.