Try   HackMD

CS193P - Lecture 4: Memorize Game Logic - 84 mins

  • Demo code at cs193p.stanford.edu
  • Architecture
    • MVVM - use ViewModel to bind the view to model
    • protocol Identifiable
    • Struct's mutating function
    • behide protcol ObserableObject
    • type Enum

02:56 build a ViewModel

If we think of a View as just an agent for showing what in the Model through the ViewModel, then more likely, we're gonna pass it to it as an argument.

// Before struct ContentView: View { var emojis = [ "🛸", "🚛", "✈️", "🚢", "🚓", "⛵️", "🚁", "🚂", "🚃", "🚀", "🏎", "🚲", "🛵", "🚘" ] @State var emojiCount = 10 var body: some View { LazyVGrid(columns: ...) { ForEach(emojis[0..<emojiCount], id: \.self) { emoji in ... } } } }
// After struct ContentView: View { @ObservedObject var viewModel: EmojiMemoryGame var body: some View { LazyVGrid(columns: ...) { ForEach(viewModel.cards) { card in ... } } } }

12:40 identifiable

// Before struct Card { var isFaceUp: Bool = false var isMatched: Bool = false var content: CardContent }
// After struct Card: Identifiable { var isFaceUp: Bool = false var isMatched: Bool = false var content: CardContent var id: Int }

31:30 mutating func

struct MemoryGame<CardContent> where CardContent: Equatable { var cards: Array<Card> mutating func choose(_ card: Card) { ... cards[chosenIndex].isMatched = true } }

33:18 conform ObservableObject

讓 ViewModel conform protocol ObservableObject ⬅️ can publish somthing changed

public protocol ObservableObject : AnyObject { /// The type of publisher that emits before the object has changed. associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never /// A publisher that emits before the object has changed. var objectWillChange: Self.ObjectWillChangePublisher { get } }
// Before class EmojiMemoryGame: ObservableObject { private var model = createMemoryGame() func choose(_ card: Card) { obhectWillChange.send() model.choose(card) } }
// After class EmojiMemoryGame: ObservableObject { @Published private var model = createMemoryGame() func choose(_ card: Card) { // obhectWillChange.send() // 因為我們已經加上 @Published,所以任何地方任何人更動了 model,都會自動幫我們呼叫 obhectWillChange.send() model.choose(card) } }

SwiftUI will automatically monitor for such changes, and re-invoke the body property of any views that rely on the data.
(hackingwithswift) What is the @Published property wrapper?

35:49 @ObservedObject

  • @ObservedObject means that when this says something changed, please rebuild my entire body.
  • can only be applied to a var

➡️ 完成「一旦 Model 更新,View 自動連動更新」

37:40 Enum

  • enums only have discrete states
  • an enum is a good data structure to represent that because there's no other options.
enum FastFoodMenuItem { case hamburger, fries, drink, cookie }

39:03 associated data

enum FastFoodMenuItem { case .hamburger(numberOfPatties: Int) case .fries(size: FryOrderSize) case .drink(String, ounces: Int) case .cookie }

41:40 checking an enum's state

var menuItem = FastFoodMenuItem.drink("coke", ounces: 32) switch menuItem { case .hamburger(let pattyCount): print("aburger 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!") }

48:02 CaseIterable

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

enum FastFoodMenuItem: CaseIterable { ... } for item in FastFoodMenuItem.allCases { ... }

51:09 optional

An Optional is just an enum.

enum Optional<T> { case none case some(T) }

1:00:10 optional chaining

let x: String? = ... let y = x?.foo()?.bar?.z

1:02:35 Higher-Order-Functions 高階函數

func index(of card:) 換成 firstIndex(where: )

// Before func index(of card: Card) -> Int { for index in 0..<cards.count { if cards[index].id == card.id { return index } } return 0 // bogus! }
// After cards.firstIndex(where: { $0.id == card.id })

1:11:04

開始思考如何「讓場上至多兩張卡片是翻開」的遊戲規則

mutating func choose(_ card: Card) { if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }), !cards[chosenIndex].isFaceUp, !cards[chosenIndex].isMatched { if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard { 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.toggle() } print("\(cards)") }

1:23:50

兩兩對應的卡片被翻開後 UI 沒有更新,卡片又被蓋回去了
→ view 那邊去檢查 isMatched 狀態

struct CardView: View { let card: MemoryGame<String>.Card var body: some View { let shape = RoundedRectangle(cornerRadius: 20) if card.isFaceUp { shape.fill().foregroundColor(.white) } else if card.isMatched { // 這邊檢查,已經解答的卡片讓他們隱藏 shape.opacity(0) } else { shape.fill() } } }

1:24:52 Recap

Model R&R

  • 弄出 Model (MemoryGame<CardContent>),跟 UI 毫無關係(不需要 import SwiftUI)
  • Model 內有 資料 (Array<Card>)、遊戲邏輯 (func choose(card:))
  • Model 是一個「事實」(Model is the truth),所有資料都記錄在那邊
  • View 僅僅只是反映出資料長什麼樣子 (reflect the current state of the Model)
  • 把 @State 拿掉了,因為不想把「狀態」這筆資料存在 View 裡面 (don't store state in our Views. We store them in the Model)

ViewModel R&R

  • ViewModel (EmojiMemoryGame) 充當畫面資料提供的轉譯者
  • The ViewModel also enables the entire reactive architecture.

Binding View & ViewModel

  • 注意: Model 需要是一個 struct 型別,Swift 才能偵測到東西被改變了
  • conform ObservableObject 的物件,透過 objectWillChange.send() 可以讓整個世界知道被改變了
  • 使用 @Published,讓「通知改變」這件事自動發生
  • 接著,加上 @ObservedObject,SwiftUI 就會知道當資料更新時需要重新產生 View (get its body rebuilt)

View R&R

  • View 透過 intent functions 來傳遞 UI 事件

Programming Assignment 2