[SwiftUI 100天] WorldScramble · part1

貓克杯發表於2020-03-13
譯自 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()

  1. newWord 全部小寫化,移除空白字元
  2. 確保至少有 1 個字元的長度,否則就結束
  3. 把這個單詞插入到 usedWords 陣列的第 0 個位置
  4. 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及計算機程式設計的相關文章,以及優秀國外文章翻譯,歡迎關注~

[SwiftUI 100天] WorldScramble · part1


相關文章