owned this note
owned this note
Published
Linked with GitHub
# 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)