Swift 2 By Example
上QQ阅读APP看书,第一时间看更新

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.