# 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](https://youtu.be/oWZOFSYS5GE?t=176) 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. ```swift= // Before struct ContentView: View { var emojis = [ "🛸", "🚛", "✈️", "🚢", "🚓", "⛵️", "🚁", "🚂", "🚃", "🚀", "🏎", "🚲", "🛵", "🚘" ] @State var emojiCount = 10 var body: some View { LazyVGrid(columns: ...) { ForEach(emojis[0..<emojiCount], id: \.self) { emoji in ... } } } } ``` ```swift= // After struct ContentView: View { @ObservedObject var viewModel: EmojiMemoryGame var body: some View { LazyVGrid(columns: ...) { ForEach(viewModel.cards) { card in ... } } } } ``` #### [12:40](https://youtu.be/oWZOFSYS5GE?t=760) identifiable ```swift= // Before struct Card { var isFaceUp: Bool = false var isMatched: Bool = false var content: CardContent } ``` ```swift= // After struct Card: Identifiable { var isFaceUp: Bool = false var isMatched: Bool = false var content: CardContent var id: Int } ``` #### [31:30](https://youtu.be/oWZOFSYS5GE?t=1890) mutating func ```swift= struct MemoryGame<CardContent> where CardContent: Equatable { var cards: Array<Card> mutating func choose(_ card: Card) { ... cards[chosenIndex].isMatched = true } } ``` #### [33:18](https://youtu.be/oWZOFSYS5GE?t=1998) conform `ObservableObject` 讓 ViewModel conform protocol `ObservableObject` ⬅️ can publish somthing changed ```swift= 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 } } ``` ```swift= // Before class EmojiMemoryGame: ObservableObject { private var model = createMemoryGame() func choose(_ card: Card) { obhectWillChange.send() model.choose(card) } } ``` ```swift= // 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?](https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-published-property-wrapper) #### [35:49](https://youtu.be/oWZOFSYS5GE?t=2149) @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](https://youtu.be/oWZOFSYS5GE?t=2260) Enum - enums only have discrete states - an enum is a good data structure to represent that because there's no other options. ```swift= enum FastFoodMenuItem { case hamburger, fries, drink, cookie } ``` #### [39:03](https://youtu.be/oWZOFSYS5GE?t=2343) associated data ```swift= enum FastFoodMenuItem { case .hamburger(numberOfPatties: Int) case .fries(size: FryOrderSize) case .drink(String, ounces: Int) case .cookie } ``` #### [41:40](https://youtu.be/oWZOFSYS5GE?t=2500) checking an enum's state ```swift= 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](https://youtu.be/oWZOFSYS5GE?t=2882) CaseIterable Now this enum will have a static var `allCases` that you can iterate over. ```swift= enum FastFoodMenuItem: CaseIterable { ... } for item in FastFoodMenuItem.allCases { ... } ``` #### [51:09](https://youtu.be/oWZOFSYS5GE?t=3069) optional An `Optional` is just an enum. ```swift= enum Optional<T> { case none case some(T) } ``` #### [1:00:10](https://youtu.be/oWZOFSYS5GE?t=3610) optional chaining ```swift= let x: String? = ... let y = x?.foo()?.bar?.z ``` ## [1:02:35](https://youtu.be/oWZOFSYS5GE?t=3755) Higher-Order-Functions 高階函數 `func index(of card:)` 換成 `firstIndex(where: )` ```swift= // Before func index(of card: Card) -> Int { for index in 0..<cards.count { if cards[index].id == card.id { return index } } return 0 // bogus! } ``` ```swift= // After cards.firstIndex(where: { $0.id == card.id }) ``` #### [1:11:04](https://youtu.be/oWZOFSYS5GE?t=4264) > 開始思考如何「讓場上至多兩張卡片是翻開」的遊戲規則 ```swift= 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](https://youtu.be/oWZOFSYS5GE?t=5030) 兩兩對應的卡片被翻開後 UI 沒有更新,卡片又被蓋回去了 → view 那邊去檢查 isMatched 狀態 ```swift= 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](https://youtu.be/oWZOFSYS5GE?t=5092) 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](https://cs193p.sites.stanford.edu/sites/g/files/sbiybj16636/files/media/file/Assignment%202.pdf)