I'm learning iOS through this past Stanford CS193P course. I've just completed the first assignment and would love a code review as I can't actually turn in an assignment for feedback since the course has already completed.
The requirements can be found here but any suggestions on best practices and my implantation would be helpful. The app works and is in a state I would submit if I could. I'm just looking for another set of eyes.
import Foundation
/**
a game card that contains the following var:
- isFaceUp
- isMatched
- identifier
*/
struct Card: Hashable
{
var hashValue: Int {
return identifier
}
static func == (lhs: Card, rhs: Card) -> Bool {
return lhs.identifier == rhs.identifier
}
var isFaceUp = false
var isMatched = false
private var identifier: Int
private static var identifierFactory = 0
private static func getUniqueIdentifer() -> Int {
identifierFactory += 1
return identifierFactory
}
init(){
self.identifier = Card.getUniqueIdentifer()
}
}
import Foundation
struct Concentration
{
private(set) var cards = [Card]()
private(set) var score = 0
private(set) var flipCount = 0
private var indexOfOneAndOnlyFaceUp: Int? {
get {
return cards.indices.filter({cards[$0].isFaceUp}).oneAndOnly
}
set {
for flipDownIndex in cards.indices {
cards[flipDownIndex].isFaceUp = (flipDownIndex == newValue)
}
}
}
private var selectedIndex = Set<Int>()
private var lastIndexWasSelected = false
/// returns true if all cards have been matched
var allCardsHaveBeenMatched: Bool {
for index in cards.indices {
if !cards[index].isMatched { return false }
}
return true
}
/**
Choose a card at an index.
Handles flip count, if a card is faced up, and matching of cards
*/
mutating func chooseCard(at index: Int){
assert(cards.indices.contains(index), "Concentration.chooseCard(at:\(index)): index is not in the cards")
let cardWasPreviouslySelected = selectedIndex.contains(index)
if !cards[index].isMatched {
// only flip cards that are visible
flipCount += 1
if let matchIndex = indexOfOneAndOnlyFaceUp, matchIndex != index {
// 2 cards are face up, check if cards match
if cards[index] == cards[matchIndex] {
cards[index].isMatched = true
cards[matchIndex].isMatched = true
if lastIndexWasSelected {
// add extra to account for subtracting earlier
score += 3
} else {
score += 2
}
}else {
// no match
if cardWasPreviouslySelected {score -= 1}
}
cards[index].isFaceUp = true
} else {
// one card is selected, turn down other cards and set this card
if cardWasPreviouslySelected { score -= 1 }
indexOfOneAndOnlyFaceUp = index
lastIndexWasSelected = cardWasPreviouslySelected
}
}
selectedIndex.insert(index)
}
init(numberOfPairsOfCards: Int){
assert(numberOfPairsOfCards > 0, "Concentraation.init(numberOfPairsOfCards:\(numberOfPairsOfCards) you must have multiple pairs of cards")
for _ in 0..<numberOfPairsOfCards {
let card = Card()
cards += [card, card]
}
shuffleCards()
}
mutating private func shuffleCards() {
for _ in 0..<cards.count {
// sort seems better than .swap()
cards.sort(by: {_,_ in arc4random() > arc4random()})
}
}
}
extension Collection {
var oneAndOnly: Element? {
return count == 1 ? first : nil
}
}
import Foundation
class Theme
{
enum Theme: UInt32 {
case halloween
case love
case animal
case waterCreatures
case plants
case weather
}
/// get an array of icons by theme
func getThemeIcons(by theme: Theme) -> [String] {
switch theme {
case .halloween:
return ["π»", "π", "πΉ", "πΊ", "π", "β οΈ", "π¦", "π"]
case .love:
return ["π","π", "π", "π", "β€οΈ", "π", "π", "π"]
case .animal:
return ["πΆ", "π", "π", "π¦", "π¦", "π", "π₯", "π¦"]
case .waterCreatures:
return ["π¦", "π", "π ", "π‘", "π³", "π", "π¦", "π¦"]
case .plants:
return ["π", "π", "πΉ", "π", "πΎ", "πΈ", "πΊ", "π»"]
case .weather:
return ["π₯", "βοΈ", "βοΈ", "π¦", "βοΈ", "π¬", "βοΈ", "πͺ", "β"]
}
}
private func random() -> Theme {
let max = Theme.weather.rawValue
let randomIndex = arc4random_uniform(max + UInt32(1))
return Theme(rawValue: randomIndex) ?? Theme.halloween
}
/**
get a random array of themed icons
- Author:
Anna
*/
func getRandomThemeIcons() ->[String] {
return getThemeIcons(by: random())
}
}
import UIKit
class ViewController: UIViewController {
private lazy var game = Concentration(numberOfPairsOfCards: numberOfPairsOfCards)
var numberOfPairsOfCards: Int {
return (cardButtons.count + 1) / 2
}
@IBOutlet weak var finishedLabel: UILabel!
@IBOutlet weak var scoreCountLabel: UILabel!
@IBOutlet weak var flipCountLabel: UILabel!
private var flipCount = 0 {
didSet {
flipCountLabel.text = "Flip Count: \(flipCount)"
}
}
private var scoreCount = 0 { didSet { scoreCountLabel.text = "Score: \(scoreCount)"} }
@IBOutlet var cardButtons: [UIButton]!
@IBAction func touchCard(_ sender: UIButton) {
if let cardNumber = cardButtons.index(of: sender){
game.chooseCard(at: cardNumber)
updateViewFromModel()
}else {
print("card is not in cardButton array")
}
}
@IBAction func touchNewGame(_ sender: UIButton) {
// reset game
game = Concentration(numberOfPairsOfCards: (cardButtons.count + 1) / 2)
// reset theme choices
emojiChoices = theme.getRandomThemeIcons()
// update view
updateViewFromModel()
}
private func updateViewFromModel(){
flipCount = game.flipCount
scoreCount = game.score
for index in cardButtons.indices {
let button = cardButtons[index]
let card = game.cards[index]
if card.isFaceUp {
button.setTitle(emoji(for: card), for: UIControlState.normal)
button.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
} else {
button.setTitle("", for: UIControlState.normal)
button.backgroundColor = card.isMatched ? #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0) : #colorLiteral(red: 1, green: 0.5763723254, blue: 0, alpha: 1)
}
}
finishedLabel.textColor = game.allCardsHaveBeenMatched ? #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) : #colorLiteral(red: 0.9999960065, green: 1, blue: 1, alpha: 0)
finishedLabel.text = game.score >= 0 ? "Nice work! π" : "Phew, π»ly made it"
}
private var theme = Theme()
private lazy var emojiChoices = theme.getRandomThemeIcons()
private var emoji = [Card: String]()
private func emoji(for card: Card) -> String {
if emoji[card] == nil, emojiChoices.count > 0 {
emoji[card] = emojiChoices.remove(at: emojiChoices.count.arc4random)
}
return emoji[card] ?? "?"
}
}
extension Int {
var arc4random: Int {
if self > 0 {
return Int(arc4random_uniform(UInt32(self)))
} else if self < 0 {
return -Int(arc4random_uniform(UInt32(abs(self))))
}else {
return 0
}
}
}