譯自 Word Scramble: Introduction
更多內容,歡迎關注公眾號 「Swift花園」
喜歡文章?不如點贊關注吧
介紹
這個專案會是又一個遊戲,不過遊戲的方式是我介紹更多 Swift 和 SwiftUI 知識的伎倆 ?。這個遊戲會向玩家展示一個隨機的 8 字母單詞,讓玩家從中拼出更多單詞。舉個例子,如果開始的單詞是 “alarming” ,那麼玩家可以拼出 “alarm”, “ring”,“main” (可以重新排列字母) 等等。在這裡, “alarming” 稱為 “根單詞”。
在這個過程中,你會用到 List
, onAppear()
, Bundle
, fatalError()
等等。所有將在之後的 SwiftUI 開發中經常用到的技能。你還會實踐@State
, Alert
, NavigationView
等,趁現在享受輕鬆的時光 —— 因為這是 100 天挑戰中最後一個簡單的專案了。
建立一個新的 Single View App 專案,名字叫 WordScramble 。你需要為這個工程下載一個檔案,它包含一個叫 “start.txt” 的檔案,稍後會用到。
言歸正傳,開始編碼。
介紹 List —— 你的好夥伴
這一節請移步:
https://juejin.im/post/5e54a8d9f265da57434bb917
從 app bundle 中載入資源
這一節請移步:
https://juejin.im/post/5e55d1a9f265da574b791240
處理字串
這一節請移步:
https://juejin.im/post/5e571b1e518825496038df91
往單詞列表新增東西
這個 app 的介面主要由三部分 SwiftUI 檢視構成:一個 NavigationView
用以展示根單詞,一個 TextField
用來給玩家輸入一個單詞, 一個 List
展示玩家已經輸入的單詞。
目前為止,每當使用者輸入一個單詞到文字框,我們會自動把它新增到已經使用過的單詞列表中。不過稍後我們會增加一些檢驗,確保單詞沒有被用過,並且的確能從根單詞中生成,最重要的是,確實是一個有意義的單詞而不是隨機的字母組合。
讓我們先從一些基礎的開始,我們需要一個陣列來存放已經用過的單詞,一個根單詞以及一個可以繫結到文字框的字串。把下面三個屬性新增到ContentView
:
@State private var usedWords = [String]()
@State private var rootWord = ""
@State private var newWord = ""複製程式碼
對於檢視的 body ,我們從最簡單的開始:一個以 rootWord 作為標題的 NavigationView
,裡面用 VStack
放文字框和單詞列表:
var body: some View {
NavigationView {
VStack {
TextField("Enter your word", text: $newWord)
List(usedWords, id: \.self) {
Text($0)
}
}
.navigationBarTitle(rootWord)
}
}複製程式碼
通過把 usedWords
直接傳給 List
,我們讓 SwiftUI 為陣列裡的每一個單詞建立一行,用單詞本身唯一標識。如果 usedWords
裡有很多重複的話,這樣做就會有問題。但是很快我們會解決這個問題。
執行程式,你會看到文字框看起來不是很好看 —— 它相對導航欄和列表甚至不是很清晰可見。幸運的是,我們可以利用 textFieldStyle() modifier 讓 SwiftUI 在它周圍繪製一個淺灰色的圓角邊框,再在邊緣加上一些 padding 以便它不會捱到螢幕邊緣。為文字框加上下面兩個 modifier :
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()複製程式碼
樣式看起來好多了,但是還有一個問題:雖然我們可以往文字框輸入,但是沒有地方可以提交輸入的內容 —— 沒有可以新增單詞的方法。
為了解決這個問題,我們需要寫一個新的方法,名字可以叫 addNewWord()
:
- 把
newWord
全部小寫化,移除空白字元 - 確保至少有 1 個字元的長度,否則就結束
- 把這個單詞插入到
usedWords
陣列的第 0 個位置 - 把
newWord
重新設定回空的字串
稍後我們會在步驟 2 和步驟 3 之間增加一些額外的校驗,確保單詞是被允許的,不過目前這個方法還算一目瞭然:
func addNewWord() {
// 小寫並且修剪單詞,確保我們不會因為大小寫的不同而重複
let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
// 如果字串數量為 0 則退出
guard answer.count > 0 else {
return
}
// extra validation to come
usedWords.insert(answer, at: 0)
newWord = ""
}複製程式碼
當使用者點選鍵盤上的 return 鍵時,我們希望呼叫addNewWord()
,在 SwiftUI 中,我們可以通過為文字框提供一個 on commit 閉包來實現這一點。我知道這聽取來有點高階,不過實踐上其實就是給TextField
提供一個拖尾閉包,它會在 return 點選時被呼叫。
實際上,閉包的簽名 —— 它接收的引數和返回值型別,跟我們剛剛寫的 addNewWord()
方法是匹配的,我們可以直接傳入這個方法:
TextField("Enter your word", text: $newWord, onCommit: addNewWord)複製程式碼
執行 app ,你會看到事情開始工作:我們可以輸入單詞到文字框,點選 return ,然後單詞出現在列表中:
在 addNewWord()
中我們之所以用 usedWords.insert(answer, at: 0)
是有原因的。如果我們用的是 append(answer)
,那麼新的單詞會出現列表的末尾,很有可能超出螢幕,但把新單詞插入到陣列的頭部則可以自動滑入到列表頭部,這樣的設定更好。
在我們把標題放進導航檢視前,我要對我們的佈局做兩個小改動。
首先,當我們呼叫 addNewWord()
時,它把使用者輸入的單詞小寫化,這樣可以避免使用者新增 “car”, “Car”,和 “CAR” 這種重複的單詞。但是,實踐上有一個地方會很奇怪:文字框會自動把使用者輸入的任何單詞的首字母變成大寫,而使用者提交 “Car” 的時候會在列表中 看到 “car” 。
為了解決這個問題,我們可以禁用文字框的自動大寫功能,用到又一個 modifier: autocapitalization()
,把這一行加到文字框控制元件:
.autocapitalization(.none)複製程式碼
第二個要做的改動 (僅僅是因為我們可以做這件事),是用 Apple 的 SF Symbols 圖示在每個單詞的文字旁邊顯示這個單詞的長度。SF Symbols 提供用圓表示的從 0 到 50 的數字, 全部以 “x.circle.fill” 的格式命名,也就是 1.circle.fill,20.circle.fill 。
在這個程式我們會向使用者展示 8 字母的單詞,所以如果他們重新排列一個新單詞,最長也不會超過 8 個字母。因此,我們用 SF Symbols 的圓形數字是肯定沒問題的 —— 因為我們知道所有可能的數字長度都可以覆蓋到。
如果我們在一個 List
的行裡用到第二個檢視, SwiftUI 會為我們自動建立一個隱式的水平 stack ,以便檢視在行裡面並排在一起。也就是說,我們可以直接新增一個 Image(systemName:)
到 List 裡:
List(usedWords, id: \.self) {
Image(systemName: "\($0.count).circle")
Text($0)
}複製程式碼
再次執行 app ,你會發現當你在文字框裡輸入單詞,然後點選 return 時,新單詞會滑入列表,並且帶有長度標識。?
我的公眾號 這裡有Swift及計算機程式設計的相關文章,以及優秀國外文章翻譯,歡迎關注~