百日學 Swift(Day 25) – Consolidation II, Milestone: Projects 1-3(第 2 階段總結,里程碑:項目 1-3)
1. 學習內容
截至目前,已經完成了兩個 SwiftUI 項目和一個技術項目,後面依舊會是這樣的節奏繼續進行。今天來回憶一下第 2 階段學到的內容。
- 構建包括文本與控件(如
Picker
)的滾動表單,SwiftUI會變成漂亮的基於表格的佈局,還可以通過滑動出新屏幕做出新的選擇。 - 創建一個
NavigationView
並給予一個標題,不僅能夠將新的視圖推送到屏幕上,還能夠設置標題並避免內容出現問題。 - 使用
@State
存儲變化的數據及這樣做的原因。記住,所有的 SwiftUI 視圖都是結構體,這意味着如果沒有類似@State
的內容就無法更改它們。 - 爲
TextField
和Picker
等用戶界面控件創建雙向綁定,瞭解如何使用$variable
讀取和寫入值。 - 使用
ForEach
循環快速創建大量視圖。 - 使用
VStack
,HStack
和ZStack
構建複雜的佈局,並將它們組合在一起以構成網格。 - 將顏色和漸變可以用作視圖,還可以爲它們指定特定的 frame,以便控制它們的大小。
- 通過提供一些文本或圖像以及點擊按鈕時執行的尾隨閉包來創建按鈕。
- 通過定義顯示警報的條件來創建警報,然後從其他位置切換該狀態。
- SwiftUI 如何(以及爲什麼!)廣泛使用不透明的結果類型(
some View
),與修飾器緊密地聯繫起來讓修飾器的順序變得非常重要。 - 如何使用三元運算符創建條件修飾器,這些條件修飾器根據程序狀態應用不同的結果。
- 如何使用視圖組合和自定義視圖修飾器將代碼分解爲小部分,從而能夠構建更復雜的程序而不會丟失代碼。
2. 知識要點
- 結構體和類
- 使用 ForEach
- 使用綁定
3. 挑戰
嘗試實現“石頭、剪刀、布”的遊戲。
(1)爲出拳創建結構體並將三種拳放入到一個數組裏面備用,這個要在 ContentView 外面做,不然很多報錯,我還沒試出來怎麼放在裏面去。
struct Fist { //出拳
var id: Int // 編號,用於計算結果
var imageName: String = "" // 圖片名稱
var name: String = "" // 手勢名稱
}
let fistArray = [ // 所有出拳的情況初始化爲數組
Fist(id: 0, imageName: "cube.fill", name: "石頭"),
Fist(id: 1, imageName: "scissors", name: "剪刀"),
Fist(id: 2, imageName: "stop.fill", name: "布")
]
其中,每種拳的圖片暫時借用了 SF 符號簡單顯示了,今後如果要使用其他圖片,只要將圖片文件複製到 Assets.xcassets
文件夾,然後把 imageName 替換成對應的名字即可。
(2)搭建視圖
- VStack 包裹全部內容
- 標題
- HStack 包裹兩張卡片,顯示 AI 和玩家的出拳,使用子視圖
- HStack 包裹三個按鈕,分別代表三種拳,使用循環生成
- 本局比賽結果
- 比賽統計
- 復位按鈕
(3)子視圖
- VStack
- 比賽方名字
- 出拳圖片
- 拳名字
參數包括:玩家名字和出拳
(4)從視圖中逐步找到需要的狀態變量
@State var aiFist: Fist = Fist(id: -1) // ai出拳
@State var playerFist: Fist = Fist(id: -1) // 玩家出拳
@State var lastAiFist: Fist = Fist(id: -1) // 上次 AI 的出拳,目的是在下一次比賽前保持畫面
@State var result: String = "" // 單局比賽結果
@State var games = 0 // 總局數
@State var playerWin = 0 // 玩家勝局數
@State var evenGames = 0 // 平局數
(5)更新主視圖中調用子視圖的語句,加上應該傳入的參數
HStack(spacing: 20) {
card(playerName: "AI", fist: lastAiFist) //抽取子視圖,必須傳 lastAiFist,才能保證下次比賽前畫面不刷新
card(playerName: "Player", fist: playerFist)
}
(6)更新 Button 中的 action 內容
Button(action: { // 按鈕響應動作
self.aiFist = fistArray[Int.random(in: 0...2)] // ai出拳爲隨機
self.lastAiFist = self.aiFist // 賦值給 lastAiFist 供顯示
self.playerFist = fistArray[index] // 根據按鈕點擊的 index 確定玩家出拳
self.pk(ai: self.lastAiFist, player: self.playerFist) // 判斷比賽結果
}) {
Image(systemName: fistArray[index].imageName) // 按鈕顯示的圖片
.resizable()
.aspectRatio(contentMode: .fit)
}
(7)更新顯示比賽結果的 Text
Text("本局結果:\(result)")
.font(.title)
Text("共 \(games) 局,玩家勝 \(playerWin) 局,平 \(evenGames) 局")
(8)更新復位按鈕的響應代碼
Button(action: { // 復位按鈕
self.games = 0
self.playerWin = 0
self.evenGames = 0
}){
Text("重新開始")
}
應該沒問題了!挑戰成功!!
4. 挑戰收穫
- 使用
id: Int
來判定勝負,代碼上好寫。換成字符串就麻煩了。 - 視圖中使用的結構體和一些不變的內容儘量寫在視圖以外,這個還需要進一步研究。
- ForEach 循環生成按鈕的範圍值不能使用閉合範圍,否則報錯
Cannot convert value of type 'ClosedRange<Int>' to expected argument type 'Range<Int>'
- 子視圖的抽取
- 狀態變量的設計,要根據視圖的需要逐步產生,最好有默認值,這樣 preview 時不用傳參。但有時候爲了看到某個特定的結果,也可以傳入必要的參數。
- 容器、組件、修飾器的練習