文字战争 WordScramble: 介绍
这个项目将是另一个游戏,尽管实际上这只是我偷偷摸摸地介绍更多Swift和SwiftUI知识的方法!游戏将向玩家显示一个随机的八个字母的单词,并要求他们用单词来制作单词。例如,如果入门单词为“ alarming”,则它们可能拼写为“ alarm”,“ ring”,“ main”等。
在此过程中,您将遇到List
,onAppear()
,Bundle
,fatalError()
等更多内容——未来几年将使用的所有有用技能。您还将获得@State
,Alert
,NavigationView
等更多的练习,而您应该尽可能地享受它们——这是我们最后的简单项目!
- Word Scramble 项目——List 介绍
- Word Scramble 项目——从App Bundle中加载资源文件
- Word Scramble 项目——使用字符串
首先,请创建一个名为WordScramble
的新Single View App项目。您需要下载该项目的文件,因为它包含一个名为“ start.txt”的文件,稍后您将需要它。
添加一个单词列表
该应用程序的用户界面将由三个主要的SwiftUI视图组成:一个NavigationView
显示他们正在拼写的单词,一个TextField
(他们可以输入一个答案)和一个Li
st(显示他们以前输入的所有单词)。
目前,每次用户在文本字段中输入单词时,我们都会自动将其添加到已用单词列表中。不过,稍后,我们将添加一些验证,以确保以前从未使用过该词,它实际上可以从给出的初始单词中产生,并且是真实词,而不仅仅是一些随机字母。
让我们从基础开始:我们需要一个已经使用过的单词数组,一个初始单词来为来提供拼写其他单词的字母,以及一个可以绑定到文本输入框的字符串。因此,现在将这三个属性添加到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
,我们要求它将数组中的每个单词排一行,并由单词本身作为唯一标识。如果usedWord
中有很多重复项,这会引起问题,但是很快我们将不允许这样做,所以这不是问题。
如果您运行该程序,则会看到该文本字段看起来不太好——在导航栏或列表旁边甚至看不到它。幸运的是,我们可以通过要求SwiftUI使用textFieldStyle()
修饰符在其边缘周围绘制浅灰色边框来解决此问题。通常,这看起来最好,边缘周围有一些填充物,因此不会碰到屏幕的边缘,因此,现在将这两个修饰符添加到文本字段中:
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
这种样式看起来好一点,但是文本视图仍然有一个问题:尽管我们可以在文本框中键入内容,但是我们不能从文本框中提交任何内容——无法将条目添加到已用单词列表中。
为了解决这个问题,我们将编写一个名为addNewWord()
的新方法,它将:
- 将
newWord
转换为小写并删除任何空格 - 检查它是否至少有1个字符,否则退出
- 在
usedWords
数组的0位置插入该单词 - 将
newWord
设置回空字符串
稍后,我们将在步骤2和步骤3之间添加一些额外的验证,以确保单词是允许的,但目前这种方法很简单:
func addNewWord() {
// lowercase and trim the word, to make sure we don't add duplicate words with case differences
let answer = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
// exit if the remaining string is empty
guard answer.count > 0 else {
return
}
// extra validation to come
usedWords.insert(answer, at: 0)
newWord = ""
}
我们希望在用户按下键盘上的return键时调用addNewWord()
,在SwiftUI中,我们可以通过为文本字段提供 onCommit
闭包来实现这一点。我知道这听起来很花哨,但实际上,这只是为TextField
提供一个尾随闭包的问题,只要按下return键,就会调用这个闭包。
事实上,由于闭包的签名(它需要接受的参数和它的返回类型)与我们刚刚编写的addNewWord()
方法完全匹配,我们可以直接将其传入:
TextField("Enter your word", text: $newWord, onCommit: addNewWord)
现在运行这个应用程序,你会发现事情已经开始有了进展:我们现在可以在文本字段中键入单词,按return,然后看到它们出现在列表中。
在addNewWord()
中,我们使用usedWords.insert(answer,at:0)
有一个原因:如果我们使用append(answer)
,这些新词可能会出现在列表的末尾,而它们可能不在屏幕上,但是通过在数组的开头插入新词,它们会自动滑入列表的顶部,这会更好。
在我们在导航视图中放置标题之前,我将对我们的布局进行两个小的更改。
首先,当我们调用addNewWord()
时,它将用户输入的单词小写,这很有帮助,因为它意味着用户不能添加“car”、“Car”和“CAR”。然而,这在实践中看起来很奇怪:无论用户键入什么,文本字段都会自动大写第一个字母,因此当他们提交“Car”时,他们在列表中看到的是“car”。
要解决此问题,可以使用另一个修饰符autocapitalization()
禁用文本字段TextField
的大写。请现在将此添加到文本字段:
.autocapitalization(.none)
第二件事,我们会改变,因为我们可以,是使用苹果的SF符号图标显示每个单词的长度旁边的文字。SF符号提供从0到50的圆形数字,所有这些数字都使用“x.circle.fill”格式命名—所以我们这样写“1.circle.fill”,“20.circle.fill”。
如果我们在列表行中使用第二个视图,SwiftUI将自动为我们创建一个隐式水平堆栈,以便行中的所有内容都整齐地并排放置。这意味着我们可以直接在列表中添加图像(systemName:)
,然后完成:
List(usedWords, id: \.self) {
Image(systemName: "\($0.count).circle")
Text($0)
}
如果现在运行该应用程序,您将看到您可以在文本字段中键入单词,按return,然后看到它们滑入列表,并在旁边显示其长度图标。很好!
当APP启动时运行代码
当Xcode构建一个iOS项目时,它会将编译的程序、Info.plist文件、资源目录和任何其他资源放在一个名为bundle的目录中,然后将该bundle命名为APP名字.app
。这个“.app”扩展被iOS和苹果的其他平台自动识别,这就是为什么如果你在macOS上双击Notes.app之类的东西,它就会知道要在bundle内启动这个程序。
在我们的游戏中,我们将包含一个名为“ start.txt”的文件,其中包含10,000多个八个字母的单词,这些单词将随机选择供玩家使用。该文件已包含在您应从GitHub下载的该项目的文件中,因此请立即将start.txt拖入您的项目中。
整个项目文件地址:https://github.com/twostraws/HackingWithSwift
PS:此项目文件地址
1. 打开终端
2. cd 到你想保存的文件夹 比如 cd Desktop/
3. 输入
svn checkout https://github.com/twostraws/HackingWithSwift/trunk/SwiftUI/project5
4. 回车等待
还有对应的文件地址:
svn checkout https://github.com/twostraws/HackingWithSwift/trunk/SwiftUI/project5-files
我们已经定义了一个名为rootWord
的属性,它将包含我们希望玩家拼写的单词。我们现在需要做的是编写一个名为startGame()
的新方法,它将:
- 在我们的包中查找start.txt
- 将其加载到字符串中
- 将该字符串拆分为字符串数组,每个元素都是一个单词
- 从中选择一个随机单词分配给
rootWord
,如果数组为空,则使用合理的默认值。
这四个任务中的每一个都对应一行代码,但有一个问题:如果我们在应用程序包中找不到start.txt
,或者如果我们可以找到但无法加载它,该怎么办?在这种情况下,我们有一个严重的问题,因为我们的应用程序是真的坏了——要么我们忘记包含该文件(在这种情况下,我们的游戏不会工作),或我们包含它,但由于某种原因,iOS拒绝让我们读取它(在这种情况下,我们的游戏不会工作,我们的应用程序是坏的)。
不管是什么原因造成的,这种情况永远不应该发生,Swift给了我们一个名为fatalError()
的函数,让我们能够真正清楚地检测问题。当我们调用fatalError()
时,它将无条件且始终导致我们的应用程序崩溃。它会死崩溃的。不是“可能会崩溃”或“也许会崩溃”:它总是直接终止。
我知道这听起来很糟糕,但是它让我们做的事情很重要:对于像这样的问题,比如我们忘记在项目中包含一个文件,没有必要让我们的应用在崩溃的状态下挣扎。最好是立即终止,并对出现的问题给出明确的解释,这样我们就可以纠正问题,而fatalError()
正是这样做的。
无论如何,让我们看看代码——我添加了与上面的数字匹配的注释:
func startGame() {
// 1. Find the URL for start.txt in our app bundle
if let startWordsURL = Bundle.main.url(forResource: "start", withExtension: "txt") {
// 2. Load start.txt into a string
if let startWords = try? String(contentsOf: startWordsURL) {
// 3. Split the string up into an array of strings, splitting on line breaks
let allWords = startWords.components(separatedBy: "\n")
// 4. Pick one random word, or use "silkworm" as a sensible default
rootWord = allWords.randomElement() ?? "silkworm"
// If we are here everything has worked, so we can exit
return
}
}
// If were are *here* then there was a problem – trigger a crash and report the error
fatalError("Could not load start.txt from bundle.")
}
现在我们已经有了一个方法来加载游戏的所有内容,我们需要在显示视图时实际调用这个东西。SwiftUI为我们提供了一个专用的视图修饰符,用于在显示视图时运行闭包,因此我们可以使用该修饰符调用startGame()
让游戏开始——在navigationBarTitle()
之后添加此修饰符:
.onAppear(perform: startGame)
如果你现在运行游戏,你应该在导航视图的顶部看到一个随机的8个字母的单词。它还没有真正的意义,因为玩家仍然可以输入他们想要的任何单词。让我们下一步解决这个问题…
使用UITextChecker校验单词
现在我们的游戏已经设置好了,这个项目的最后一部分是确保用户不能输入无效的单词。我们将用四个小方法来实现这一点,每个方法都执行一个检查:单词是original(它还没有被使用过)、单词是possible(它们没有试图从“ silkworm”中拼写“car”)同时还是是real(它是一个真正的英语单词)。
如果你注意的话,你会注意到只有三种方法——那是因为第四种方法会用来使显示错误消息变得更容易。
不管怎样,让我们从第一个方法开始:这将接受一个字符串作为它的唯一参数,并返回true或false,这取决于这个词以前是否使用过。我们已经有一个usedWords
数组,因此我们可以将单词传递到它的contains()
方法中,并按如下方式发送结果:
func isOriginal(word: String) -> Bool {
!usedWords.contains(word)
}
第一个方法完成啦!
下一个稍微复杂一点:我们如何检查一个随机单词是否可以由另一个随机单词的字母构成?
我们有两种方法可以解决这个问题,但最简单的方法是:如果我们创建一个词根的变量副本,那么我们可以在用户输入单词的每个字母上循环,以查看该字母是否存在于我们的副本中。如果是,我们将其从副本中删除(因此不能使用两次),然后继续。如果最后我们成功地完成了用户的单词那么这个单词是好的,否则就有错误,我们返回false。
所以,下面就是我们的第二个方法:
func isPossible(word: String) -> Bool {
var tempWord = rootWord
for letter in word {
if let pos = tempWord.firstIndex(of: letter) {
tempWord.remove(at: pos)
} else {
return false
}
}
return true
}
最后的一个方法比较困难,因为我们需要使用UIKit中的UITextChecker
。为了安全地将Swift字符串连接到Objective-C字符串,我们需要使用Swift字符串的UTF-16计数创建一个NSRange
实例。我知道这不好,但恐怕在苹果清理这些api之前,这是不可避免的。
所以,我们的最后一个方法将生成UITextChecker
的一个实例,它负责扫描字符串中拼写错误的单词。然后,我们将创建一个NSRange
来扫描字符串的整个长度,然后在文本检查器上调用rangeOfMisspelledWord()
,以便它查找错误的单词。完成后,我们将返回另一个NSRange
,告诉我们在哪里找到拼写错误的单词,但如果单词是正确的,则该范围的位置将是特殊值NSNotFound
。
所以,这是我们最后的方法:
func isReal(word: String) -> Bool {
let checker = UITextChecker()
let range = NSRange(location: 0, length: word.utf16.count)
let misspelledRange = checker.rangeOfMisspelledWord(in: word,
range: range,
startingAt: 0,
wrap: false,
language: "en")
return misspelledRange.location == NSNotFound
}
在我们使用这三个之前,我想添加一些代码,使显示错误警报更容易。首先,我们需要一些属性来控制警报:
@State private var errorTitle = ""
@State private var errorMessage = ""
@State private var showingError = false
现在,我们可以添加一个方法,该方法根据接收到的参数设置标题和消息,然后将showingError
布尔值翻转为true:
func wordError(title: String, message: String) {
errorTitle = title
errorMessage = message
showingError = true
}
然后我们可以直接在 .onAppear()
下面添加一个alert()
修饰符将它们直接传递给SwiftUI:
.alert(isPresented: $showingError) {
Alert(title: Text(errorTitle), message: Text(errorMessage), dismissButton: .default(Text("OK")))
}
我们已经做了好几次了,所以希望它能成为我们的习惯!
终于到了结束游戏的时候了:将addNewWord()
中的//extra validation to come comment
替换为:
guard isOriginal(word: answer) else {
wordError(title: "Word used already", message: "Be more original")
return
}
guard isPossible(word: answer) else {
wordError(title: "Word not recognized", message: "You can't just make them up, you know!")
return
}
guard isReal(word: answer) else {
wordError(title: "Word not possible", message: "That isn't a real word.")
return
}
如果你现在运行这个应用程序,你会发现它会拒绝让你使用那些没有通过我们测试的单词——重复一个单词是行不通的,不能从词根拼写出来的单词也行不通,胡言乱语也行不通。
这是另一个应用程序——good job!
译自 Hacking with iOS: SwiftUI Edition - Word Scramble
Word Scramble: Introduction
Adding to a list of words
Running code when our app launches
Validating words with UITextChecker
Previous: BetterRest 项目——挑战 | Hacking with iOS: SwiftUI Edition | Next: Word Scramble 项目——挑战 |
---|
赏我一个赞吧~~~