#4–2, SwiftUI,版面配置、配牌、MVVM搭建

Syuan
彼得潘的 Swift iOS / Flutter App 開發教室
7 min readNov 28, 2022
就是偷懶用重複封面圖…

前情提要:

#4–1, SwiftUI 從零開始:大老二. 怎麼就變成SwiftUI了? | by Syuan | 彼得潘的 Swift iOS App 開發教室 | Nov, 2022 | Medium

上回已經能讓四家手牌顯示了,這次要再分出一個區域可以顯示出牌。

GeometryReader

可以使用這個功能去計算自動分配三組電腦玩家應該要配置的高和寬。

使用if 來設定,只要不是手動玩家(playerIsMe),就顯示在指定範圍內,而下方的 .frame(height: geo.size.height / 6) 則是把全區域給分成六等分,這樣一來上半部就會剛好分給三個電腦玩家。

而下半部首先使用 Rectangle 切出一塊矩形區域給出牌區域,接著再一樣用 LazyVGrid 分配最後的區域給手動玩家,如下。

基本版面配置結束。

Game Logic

先安排好三個電腦跟手動玩家,這裡使用 private,這樣 player 就只會在這個 model 裡面更新不受外部影響:

struct BigTwo {
private(set) var players: [Player]

init() {
let opponents = [
Player(),
Player(),
Player()
]
players = opponents
players.append(Player(playerIsMe: true))
}
}

然後要把52張牌發給玩家,所以要設計一個Deck來生成牌堆,別忘了數字與花色的 struct 要先加上 CaseIterable, 原理這裡就不再覆述:

struct Deck {
private var cards = Stack()

mutating func createFullDeck() {
for suit in Suit.allCases {
for rank in Rank.allCases {
cards.append(Card(rank: rank, suit: suit))
}
}
}

mutating func shuffle() {
cards.shuffle()
}

mutating func drawCard() -> Card {
//抽牌的同時移除陣列最後元素/牌
return cards.removeLast()
}

func cardsRemaining() -> Int {
//抽牌之後剩餘牌數
return cards.count
}
}

有了牌堆後就要發牌,我使用 arc4random 來隨機決定第一個被發牌的玩家,arc4random 這個函數返回的數字必定在 0 ~ 2 ^ 32–1。(總數是 2 的 32 次方,但是從 0 開始算),因此在這個範圍抽到的數字如果可以整除4,那麽表示每次配牌時,被選到的四個玩家機率都會是一樣的。反之如果不能整除四,那就會有一個玩家的機率比較高:

struct BigTwo {
private(set) var players: [Player]

init() {
let opponents = [
Player(),
Player(),
Player()
]
players = opponents
players.append(Player(playerIsMe: true))

var deck = Deck()
deck.createFullDeck()
deck.shuffle()

//隨機配置玩家順序以配牌
let randomStartingPlayerIndex = Int(arc4random()) % players.count

//牌堆還有牌才抽
while deck.cardsRemaining() > 0 {
for p in randomStartingPlayerIndex...randomStartingPlayerIndex + (players.count - 1) {
let i = p % players.count // 循環 0,1,2,3,0,1,2,3,0,1,2,3...
var card = deck.drawCard() //從牌堆抽出牌
players[i].cards.append(card) //發牌給玩家
}
}
}

}

現在我已經有牌可以發,就不需要用 testPlayer 的測試,實際配到 view 來看看效果。回到view,並將剛剛做好的遊戲邏輯放到新命名的變數model,並放入 model.player,要記得將 player 的手牌改回 stack(),且 cardView 的命名也要更動。

typealias Stack = [Card]

struct Player: Identifiable {
var cards = Stack()
var playerIsMe = false
var id = UUID()
}
struct CardView: View {
let card: Card
var body: some View {
Image(card.filename)
.resizable()
.aspectRatio(2/3, contentMode: .fit)
}
}

重新整理幾次試試可以發現四家手牌已經都是52張隨機配牌。

MVVM

Model-View-ViewModel

目前為止的做法會讓 model 產生 copy 到 view,他不像 class 一樣會存在記憶體裡面的某個區域,所以會得到的是 pointer

但我不要 model 搞個 copy 出來怎麼辦呢?那就是再做一個 View Model 來存放資料。

把資料存在 View Model ,View 會觀察 View Model 中的資料。當使用者操作View畫面時,View會接收事件交給 View Model ,View Model 操作 Model 處理資料,Model 處理完資料後通知 View Model 更新資料。由於 View 會觀察 View Model 的資料,所以資料更新後 View 就會收到更新,並更新畫面。

新建一個檔案 BigTwoGame ,並且用 private 把 model 藏起來,這樣 view就只能透過 View Model 連結:

class BigTwoGame {
private var model = BigTwo()

var players: [Player] {
return model.players
}
}

實際的畫面就可以把 model.player 改成 bigTwo.players

這樣就改起來了,下回來做手牌點選。

--

--