The game screen

Before implementing the game, let's proceed to build the layout of the cards on the table.

The structure

Now let's implement a new class called MemoryViewController, which extends the UIVewController class. This will be used to manage the actual view where the Memory Game will be played. The first thing we do is add the class life cycle functions:

class MemoryViewController: UIViewController {
    private let difficulty: Difficulty

    init(difficulty: Difficulty) {
        self.difficulty = difficulty
        super.init(nibName: nil, bundle: nil)
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit{
        print("deinit")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }
}
// MARK: Setup
private extension MemoryViewController {
    func setup() {
        view.backgroundColor = .greenSea()
    }
}

Besides the initializer that accepts the chosen difficulty, although it's not used, we need to add the required initializer with NSCoder. Moreover, you should note that we need to call the parent initializer with nibName and the bundle, used when UIViewController is built from an XIB file. If we call a plain super.init() function, we will receive a runtime error because the empty one is a convenience initializer, an initializer that calls a required initializer in the same class that, in our case, is not implemented.

Although not mandatory, we have implemented the deinitializer as well, inserting just a debug log statement to verify that the class is correctly removed from the memory when dismissed. Thus, a retain cycle is avoided.

Finally, we come to this comment:

// MARK: Setup

This is a special comment that tells Xcode to present the sentence in the structure of a class in order to facilitate navigation to a different part of the class.

The last element of the status bar of the code editor of Xcode must be selected.

After this, a menu with all the functions appears, with a bold entry where we put the //MARK: comment.

Adding a collection view

Let's move on to implementing the layout of the card. We'll use UICollectionView to lay the cards on the table. UICollectionView is a view that arranges the contained cells to follow a layout we set during the setup. In this case, we set a flow layout in which each card follows the previous one, and it creates a new row when the right border is reached.

We set the properties for the view and a model to fulfill the collection view:

private var collectionView: UICollectionView!
private var deck: Array<Int>!

Next, we write the function calls to set up everything in viewDidLoad so that the functions are called when the view is loaded:

override func viewDidLoad() {
    super.viewDidLoad()
    setup()
}

The setup() function basically creates and configures CollectionView:

// MARK: Setup
private extension MemoryViewController {
    func setup() {
        view.backgroundColor = .greenSea()

        let space: CGFloat = 5
        let (covWidth, covHeight) = collectionViewSizeDifficulty(difficulty, space: space)
        let layout = layoutCardSize(cardSizeDifficulty(difficulty, space: space), space: space)

        collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: covWidth, height: covHeight),collectionViewLayout: layout)
        collectionView.center = view.center
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.scrollEnabled = false
        collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "cardCell")
        collectionView.backgroundColor = .clearColor()

        self.view.addSubview(collectionView)
    }

After setting the color of the collectionview, we define a constant, space, to set the space between every two cards.

Next, we calculate the size of the collectionview given the difficulty, and hence, the number of rows and columns; then, the layout. Finally, we put everything together to build the collectionview:

    func collectionViewSizeDifficulty(difficulty: Difficulty, space: CGFloat) -> (CGFloat, CGFloat) {
        let (columns, rows) = sizeDifficulty(difficulty)
        let (cardWidth, cardHeight) = cardSizeDifficulty(difficulty, space: space)

        let covWidth = columns*(cardWidth + 2*space)
        let covHeight = rows*(cardHeight + space)
        return (covWidth, covHeight)
    }

The cardSizeDifficulty() function calculates the size of the collection view by multiplying the size of each card by the number of rows or columns:

    func cardSizeDifficulty(difficulty: Difficulty, space: CGFloat) -> (CGFloat, CGFloat) {
        let ratio: CGFloat = 1.452

        let (_, rows) = sizeDifficulty(difficulty)
        let cardHeight: CGFloat = view.frame.height/rows - 2*space
        let cardWidth: CGFloat = cardHeight/ratio
        return (cardWidth, cardHeight)
    }

The sizeDifficulty()function will be introduced later; just to make it buildable, let's implement it with only one hardcoded value:

    func sizeDifficulty(difficulty: Difficulty) -> (CGFloat, CGFloat) {
        return (4,3)
    }

Because the column value returned by the sizeDifficulty()function is not used anywhere, we can safely associate it with the wildcard keyword _.

Sizing the components

As mentioned at the start of this chapter, we are not using Auto Layout, but we need to handle the issue of different screen sizes somehow. Hence, using basic math, we adapt the size of each card to the available size on the screen:

    func layoutCardSize(cardSize: (cardWidth: CGFloat, cardHeight: CGFloat), space: CGFloat) -> UICollectionViewLayout {
        let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
        layout.sectionInset = UIEdgeInsets(top: space, left: space, bottom: space, right: space)
        layout.itemSize = CGSize(width: cardSize.cardWidth, height: cardSize.cardHeight)
        layout.minimumLineSpacing = space
        return layout
    }

As mentioned earlier, the UICollectionView class shows a series of cells in its content view, but the way in which the cells are presented—as a grid or a vertical pile—the space between them is defined by an instance of UICollectionViewFlowLayout.

Finally, we set up the layout, defining the size of each cell and how they are separated and laid out.

We have seen that there is a connection between the difficulty setting and the size of the grid of the cards, and this relation is implemented simply using switch statements:

// MARK: Difficulty
private extension MemoryViewController {
    func sizeDifficulty(difficulty: Difficulty) -> (CGFloat, CGFloat) {
        switch difficulty {
            case .Easy:
                return (4,3)
            case .Medium:
                return (6,4)
            case .Hard:
                return (8,4)
        }
    }

    func numCardsNeededDifficulty(difficulty: Difficulty) -> Int {
        let (columns, rows) = sizeDifficulty(difficulty)
        return Int(columns * rows)
    }
}