This article is related notes for the 5th episode of Stanford University's CS193p course.

cs193p class:

The lectures for the Spring 2023 version of Stanford University's course CS193p (Developing Applications for iOS using SwiftUI) were given in person but, unfortunately, were not video recorded. However, we did capture the laptop screen of the presentations and demos as well as the associated audio. You can watch these screen captures using the links below. You'll also find links to supporting material that was distributed to students during the quarter (homework, demo code, etc.).

cs193p class website: https://cs193p.sites.stanford.edu/2023


//  EmojiMemoryGameView.swift
struct EmojiMemoryGameView: View {
    @ObservedObject var viewModel: EmojiMemoryGame
        
    var body: some View {
        VStack {
            ScrollView {
                cards
                    .animation(.default, value: )
            }
            Button("Shuffle") {
                viewModel.shuffle()
            }
        }
        .padding()
    }
...

We can do an animation by adding .animation view modifier. The value means it will only animate when this value changes.

Equatable Protocol

However, we will receive an error from Swift, "Referencing instance method 'animation(_:value:)' on 'Array' requires that 'MemoryGame\<String\>.Card' conform to 'Equatable'". This means if something changed, the animation takes a copy of it. And when something also changed, it takes another copy of it. The animation will animate only when the two copy are not equal. So we need to implement our Card equitable.

//  MemorizeGame.swift
...
    struct Card: Equatable {
        static func == (lhs: Card, rhs: Card) -> Bool {
            return lhs.isFaceUp == rhs.isFaceUp &&
            lhs.isMatched == rhs.isMatched &&
            lhs.content == rhs.content
        }
        
        var isFaceUp = true
        var isMatched = false
        let content: CardContent
    }
...

We requies to write a static function that takes a function and return a Bool in order to implement Equatable protocol. The function need two paramters, a left hand side Card and a right hand side Card.

But the error message "Referencing operator function '==' on 'Equatable' requires that 'CardContent' conform to 'Equatable'" from Swift expresses we need to make our CardContent equatable. Card Content is our "don't care".

//  MemorizeGame.swift
...
struct MemoryGame<CardContent> where CardContent: Equatable {
    ...
    ...
    
    struct Card: Equatable {
        static func == (lhs: Card, rhs: Card) -> Bool {
            return lhs.isFaceUp == rhs.isFaceUp &&
            lhs.isMatched == rhs.isMatched &&
            lhs.content == rhs.content
        }
        
        var isFaceUp = true
        var isMatched = false
        let content: CardContent
    }
}

To make our CardContent equatable, we use where CardContent: Equatable to achieve it. That means, our "don't care" are cares a little bit.

struct MemoryGame<CardContent> where CardContent: Equatable {  
    ...
      ...
    
    struct Card: Equatable {
        var isFaceUp = true
        var isMatched = false
        let content: CardContent
    }
}

Another cool feature in Swift, if we just compre each thing like above, we can just remove it.

animated-does-not-right

It did works, but it looks not like card shuffle.

//  EmojiMemoryGameView.swift
...
    var cards: some View {
        LazyVGrid(columns: [GridItem(.adaptive(minimum: 85), spacing: 0)], spacing: 0) {
            ForEach(viewModel.cards.indices, id: \.self) { index in
                CardView(viewModel.cards[index])
                    .aspectRatio(2/3, contentMode: .fit)
                    .padding(4)
            }
        }
        .foregroundColor(.orange)
    }
...

Because our ForEach iterate the indices of the array. It going from card 0, 1, 2, 3 ... and make the Card view for each one. When we shuffle the card, we move the card from No. 0 to 7 for example. But the ForEach still showing the card from card index from 0, 1, 2...

We want to move the card itself, the Card view.

//  EmojiMemoryGameView.swift
...
    var cards: some View {
        LazyVGrid(columns: [GridItem(.adaptive(minimum: 85), spacing: 0)], spacing: 0) {
            ForEach(viewModel.cards, id: \.self) { card in
                CardView(card)
                    .aspectRatio(2/3, contentMode: .fit)
                    .padding(4)
            }
        }
        .foregroundColor(.orange)
    }
...

We made some changes. The code above will produce error "Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'MemoryGame\<String\>.Card' conform to 'Hashable'".

//  EmojiMemoryGameView.swift
...
    var cards: some View {
        LazyVGrid(columns: [GridItem(.adaptive(minimum: 85), spacing: 0)], spacing: 0) {
            ForEach(viewModel.cards) { card in
                CardView(card)
                    .aspectRatio(2/3, contentMode: .fit)
                    .padding(4)
            }
        }
        .foregroundColor(.orange)
    }
...

Now, it's time to talk about id: \.self. It means what I use to identify these things. self means use the thing itself, it works great for like Int or kinds of stuff. Here, we want to make the id unique. But it will not works for us, Card includes isFaceUp, isMatch all complement. When we click the card, the id changes. We need something that just to identify the that Card.

Thus, we remove the id. And it produces another error, "Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'MemoryGame\<String\>.Card' conform to 'Identifiable'".

So, we will make our card identifiable.

Identifiable Protocol

//  MemorizeGame.swift
import Foundation
struct MemoryGame<CardContent> where CardContent: Equatable {
    private(set) var cards: Array<Card>
    
    init(numberOfPairsOfCards: Int, cardContentFactory: (Int) -> CardContent) {
        cards = []
        // add numberOfParisOfCards x 2 cards
        for pairIndex in 0..<max(2, numberOfPairsOfCards) {
            let content = cardContentFactory(pairIndex)
            cards.append(Card(content: content, id: "\(pairIndex+1)a"))
            cards.append(Card(content: content, id: "\(pairIndex+1)b"))
        }
    }
    
    ...
      ...
    
    struct Card: Equatable, Identifiable {
        var isFaceUp = true
        var isMatched = false
        let content: CardContent
        
        var id: String
    }
}

We added an id typed as a String, and we assign the id when we create new cards. The id would looks like 1a, 1b, 2a, 2b, 3a, 3b, etc...

animated-does-right

Wow, it does working!

CustomDebugStringConvertible

complicated-print-message

Our currrent print message is pretty complicated. So, we can make it better.

//  MemorizeGame.swift
...
    struct Card: Equatable, Identifiable, CustomDebugStringConvertible {
        var isFaceUp = true
        var isMatched = false
        let content: CardContent
        
        var id: String
        var debugDescription: String {
            "\(id): \(content) \(isFaceUp ? "up" : "down") \(isMatched ? "matched": "")"
        }
    }
...

We implement CustomDebugStringConvertible protocol for our Card, and defined debugDescription.

custom-print-message

Now, it looks really simplified.

ViewModel Intent - Part 1

//  EmojiMemoryGameView.swift
...
    var cards: some View {
        LazyVGrid(columns: [GridItem(.adaptive(minimum: 85), spacing: 0)], spacing: 0) {
            ForEach(viewModel.cards) { card in
                CardView(card)
                    .aspectRatio(2/3, contentMode: .fit)
                    .padding(4)
                    .onTapGesture {
                        viewModel.choose(card)
                    }
            }
        }
        .foregroundColor(.orange)
    }
...
//  MemorizeGame.swift
...
    func choose(_ card: Card) {
        card.isFaceUp.toggle()
    }
...

Now, we can implement user's intent (flip the card).

We used the .onTapGesture and .toggle() try to flip the card when user touch the screen. However, because card is a value type. the choose function gets a copy of the card, so we can't .toggle() it.

//  MemorizeGame.swift
...
    mutating func choose(_ card: Card) {
        let chosenIndex = index(of: card)
        cards[chosenIndex].isFaceUp.toggle()
    }
    
    func index(of card: Card) -> Int {
        for index in cards.indices {
            if cards[index].id == card.id {
                return index
            }
        }
        
        return 0 // FIXME: bogus!
    }
...

It turns out, we will use the index of the card directly modify the cards array. We implemented a function called index to find card card index.

six-cards-face-down

Currently, if we can't find a card, we will just return 0, which is our first card. Before we know how to fix that, let's understand enum first. For now, it should works!

Enmu

  • Another variety of data structure in addition to struct and class

It can only have discrete states ...

enum FastFoodMenuItem {
    case hamburger
  case fries
  case drink
  case cookie
}

An enum is a value type (like struct), so it is copied as it is passed around.

  • Associated Data

Each state can (but does not have to) have its own "associated data" ...

enum FastFoodMenuItem {
    case hamburger(numberOfPatties: Int)
  case fries(size: FryOrderSize)
  case drink(String, ounces: Int) // the unnamed String is the brand, e.g. "Coke"
  case cookie
}

Note that the drink case has 2 pieces of associated data (one of them "unnamed")

In the example above, FryOrderSize would also probably be an enum, for example ...

enum FryOrderSize {
    case large
  case small
}
  • Setting the value of an enum

When you set the value of an enum you must provide the associated data(if any) ...

let menuItem: FastFoodMenuItem = FastFoodMenuItem.hamburger(patties: 2)
var otherItem: FastFoodMenuItem = FastFoodMenuItem.cookie

Swift can infer the type on one side of the assignment or the other (but not both) ...

let menuItem = FastFoodMenuItem.hamburger(patties: 2)
var otherItem: FastFoodMenuItem = .cookie

// Swift can't figure this out
var yetAnotherItem = .cookie
  • Checking the enum's state

An enum's state is usually checked with a switch statement ...

(Although we could use an if statement, but this is unusual if there is associated data)

var menuItem = FastFoodMenuItem.hamburger(patties: 2)
switch menuItem {
  case FastFoodMenuItem.hamburger: print("burger")
  case FastFoodMenuItem.fries: print("fries")
  case FastFoodMenuItem.drink: print("drink")
  case FastFoodMenuItem.cookie: print("cookie")
}

Note that we are ignoring the "associated data" above ... so far ...

The code would print "burger" on the console.

var menuItem = FastFoodMenuItem.hamburger(patties: 2)
switch menuItem {
  case .hamburger: print("burger")
  case .fries: print("fries")
  case .drink: print("drink")
  case .cookie: print("cookie")
}

It is not necessary to user the fully-expressed FastFoodMenuItem.fries inside the switch (since Swift can infer the FastFoodMenuItem part of that)

  • break

If you don't want to do anything in a given case, user break ...

var menuItem = FastFoodMenuItem.hamburger(patties: 2)
switch menuItem {
  case .hamburger: break
  case .fries: print("fries")
  case .drink: print("drink")
  case .cookie: print("cookie")
}

The code would print nothing on the console.

  • default

A switch must handle all possible cases (although you can default uninteresting cases) ...

var menuItem = FastFoodMenuItem.cookie
switch menuItem {
  case .hamburger: break
  case .fries: print("fries")
  default: print("other")
}

If the menuItem were a cookie, the above code would print "other" on the console.

You can switch on any type (not just enum), by the way, for example ...

let s: String = "hello"
switch s {
    case "goodbye": ...
  case "hello": ...
  default: ... // gotta have this for String because switch has to cover ALL cases
}
  • Mutiple lines allowed

Each case in a switch can be mutiple lines and does NOT fall through to the next case ...

var menuItem = FastFoodMenuItem.fries(size: FryOrderSize.large)
switch menuItem {
  case .hamburger: print("burger")
  case .fries:
      print("yummy")
      print("fries")
  case .drink:
      print("drink")
  case .cookie: print("cookie")
}

The above code would print "yummy" and "fries" on the console, but not "drink"

If you put the keyword fallthrough as the last line of a case, it will fall through.

  • What about the associated data?

Associated data is accessed through a switch statement using this let syntax

var menuItem = FastFoodMenuItem.drink("Coke", ounces: 32)
switch menuItem {
  case .hamburger(let pattyCount): print("a burger with \(pattyCount) patties")
  case .fries(let size): print("a \(size) order of fries!")
  case .drink(let brand, let ounces): print("a \(ounces)oz \(brand)")
  case .cookie: print("a cookie!")
}

Note that the local variable that retrieves the associated data can have a different name

(e.g. pattyCount above versus patties in the enum declaration)

(e.g. brand above versus not even having a name in the enum declaration)

  • Methods yes, (stored) Properties no

An enum can have methods (and computed properties) but no stored properties ...

enum FastFoodMenuItem {
  case hamburger(numberOfPatties: Int)
  case fries(size: FryOrderSize)
  case drink(String, ounces: Int)
  case cookie
  
  func isIncludedInSpecialOrder(number: Int) -> Bool { }
  var colories: Int { // switch on self and calculate caloric value here }
}

An enum's state is entriely which case it is in and that case's associated data, nothing more.

In an enum's own methods, you can test the enum's state (and get associated data) using self ...

enum FastFoodMenuItem {
  ...
  
  func isIncludedInSpecialOrder(number: Int) -> Bool {
    switch self {
      case .hamburger(let pattyCount): return pettyCount == number
      case .fries, .cookie: return true // a drink and cookie in every special order
      case .drink(_, let ounces): return ounces == 16 // & 16oz driink of any kind
    }
  }
}

Special order 1 is a single patty burger, 2 is a double patty (3 is a triple, etc.?!)

  • Getting all the cases of an enumeration
enum TeslaModel: CaseIterable {
  case X
  case S
  case Three
  case Y
}

Now, this enum will have a static var allCases that you can iterate over.

for model in TestlaModel.allCases {
  reportSalesNumbers(for: model)
}
func reportSalesNumbers(for model: TeslaModel) {
  switch model { ... }
}

Optional

An optional is just an enum. Period, nothing more.

It essentially looks like this ...

enum Optional<T> { // a generic type
  case none
  case some(T) // the some case has associated value of type T
}

You can see that it can only have two values: is set (some) or not set (none).

In the is set case, it can have some associated value tagging along (of "don't care type" T).

Where do we use Optional?

Anytime we have a value that can sometimes be "not set" or "unspecified" or "undetermined".

This happens surprisinly often.

That's why Swift introduces a lot of "syntactic sugar" to make it easy to use Optionals ...

Declaring something of type Optional<T> can be done with the syntax T?

You can then assign it the value nil (Optional.none)

Or you can assign it something of the type T (Optional.some with associated value = that value).

Note that Optionals always start out with an implicit = nil.

var hello: String?             var hello: Optional<String> = .none
var hello: String? = "hello"   var hello: Optional<String> = .some("hello")
var hello: String? = nil       var hello: Optional<String> = .none

You can access the associated value either by force (with !) ...

let hello: String? = ...
print(hello!)

switch hello {
  case .none: // raise an exception (crash)
  case .some(let data): print(data)
}

Or "safely" using if let and then using the safely-gotten associated value in { } (else allowed too).

if let safehello = hello {
  print(safehello)
} else {
  // do something else
}

switch hello {
  case .none: // raise an exception (crash)
  case .some(let safehello): print(safehello)
}

You may also use the shorter version, same as above.

if let hello {
  print(hello)
} else {
  // do something else
}

There's also ?? which does "Optional defaulting". It's called the "nil-coalescng operator"

let hello: String? = ...
let y = x ?? "foo"

switch hello {
  case .none: y = "foo"
  case .some(let data): y = data
}

Now, let's jump back to Code

ViewModel Intent - Part 2

//  MemorizeGame.swift
...
    private func index(of card: Card) -> Int? {
        for index in cards.indices {
            if cards[index].id == card.id {
                return index
            }
        }
        return nil
    }
...

Let's fix the bogus by using Optional.

//  MemorizeGame.swift
...
    mutating func choose(_ card: Card) {
        if let chosenIndex = index(of: card) {
            cards[chosenIndex].isFaceUp.toggle()
        }
    }
...

We also need to change to choose function, because chosenIndex is now Optioanal type. We can force unwrap it by using !, but our program will crash if index return nil. So, we use safe unwrap.

functions as argument

//  MemorizeGame.swift
...
    mutating func choose(_ card: Card) {
        if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }) {
            cards[chosenIndex].isFaceUp.toggle()
        }
    }
...

We don't have to self implement a function index. We can actually find the chosenIndex

Flip the cards down when NOT matched

//  MemorizeGame.swift
...
    var indexOfTheOneAndOnlyFaceUpCard: Int?

    mutating func choose(_ card: Card) {
        if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }) {
            if !cards[chosenIndex].isFaceUp && !cards[chosenIndex].isMatched {
                if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard {
                    // Two Cards Face Up
                    if cards[chosenIndex].content == cards[potentialMatchIndex].content {
                        cards[chosenIndex].isMatched = true
                        cards[potentialMatchIndex].isMatched = true
                    }
                    indexOfTheOneAndOnlyFaceUpCard = nil
                } else {
                    for index in cards.indices {
                        cards[index].isFaceUp = false
                    }
                    indexOfTheOneAndOnlyFaceUpCard = chosenIndex
                }
            }
            cards[chosenIndex].isFaceUp = true
        }
    }
...

Our game logic now looks right, but we need to hide the cards when they matched.

//  EmojiMemoryGameView.swift
...
    var body: some View {
        ZStack {
            let base = RoundedRectangle(cornerRadius: 12)
            Group {
                base.fill(.white)
                base.strokeBorder(lineWidth: 2)
                Text(card.content)
                    .font(.system(size: 200))
                    .minimumScaleFactor(0.01)
                    .aspectRatio(1, contentMode: .fit)
            }
            .opacity(card.isFaceUp ? 1 : 0)
            base.fill().opacity(card.isFaceUp ? 0 : 1)
        }
        .opacity(card.isFaceUp || !card.isMatched ? 1 : 0)
    }
...

So, we go back to our view, let matched cards opacity be 0.

Our main game logic should work now.

get+set computed property

//  MemorizeGame.swift
...
        var indexOfTheOneAndOnlyFaceUpCard: Int? {
        get {
            var faceUpCardIndices = [Int]()
            for index in cards.indices {
                if cards[index].isFaceUp {
                    faceUpCardIndices.append(index)
                }
            }
            if faceUpCardIndices.count == 1 {
                return faceUpCardIndices.first
            } else {
                return nil
            }
        }
        set {
            for index in cards.indices {
                if index == newValue {
                    cards[index].isFaceUp = true
                } else {
                    cards[index].isFaceUp = false
                }
            }
        }
    }
...

We can optimize the game logic by make indexOfTheOneAndOnlyFaceUpCard a computed property. This computed property will return the other face up cards (or nil) when being get. (something = indexOfTheOneAndOnlyFaceUpCard) When being set (indexOfTheOneAndOnlyFaceUpCard = something), it will set the cards passed into face up, all other cards face down.

//  MemorizeGame.swift
...
    mutating func choose(_ card: Card) {
        if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }) {
            if !cards[chosenIndex].isFaceUp && !cards[chosenIndex].isMatched {
                if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard {
                    // Two Cards Face Up
                    if cards[chosenIndex].content == cards[potentialMatchIndex].content {
                        cards[chosenIndex].isMatched = true
                        cards[potentialMatchIndex].isMatched = true
                    }
                } else {
                    indexOfTheOneAndOnlyFaceUpCard = chosenIndex
                }
            }
            cards[chosenIndex].isFaceUp = true
        }
    }
...

We also need to change our choose function, because our computed property does lots of thing for us.

Optimize

//  MemorizeGame.swift
...
    var indexOfTheOneAndOnlyFaceUpCard: Int? {
        get {
            let faceUpCardIndices = cards.indices.filter { index in cards[index].isFaceUp }
            return faceUpCardIndices.count == 1 ? faceUpCardIndices.first : nil
        }
        set {
            cards.indices.forEach { cards[$0].isFaceUp = (newValue == $0) }
        }
    }
...

Now, we used funtional programming to optimize our code. We can also use extension to make the code even better.

Extension

//  MemorizeGame.swift
...
import Foundation

struct MemoryGame<CardContent> where CardContent: Equatable {
    private(set) var cards: Array<Card>
    
        ...
      ...
    
    var indexOfTheOneAndOnlyFaceUpCard: Int? {
        get { cards.indices.filter { index in cards[index].isFaceUp }.only }
        set { cards.indices.forEach { cards[$0].isFaceUp = (newValue == $0) } }
    }
  
    ...
      ...
}

extension Array {
    var only: Element? {
        count == 1 ? first : nil
    }
}

We extend the array so we can use .only in our indexOfTheOneAndOnlyFaceUpCard computed property.

TOC