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) } }