译自 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” 的文件,稍后会用到。
言归正传,开始编码。
这一节请移步:
https://juejin.im/post/5e54a8d9f265da57434bb917
这一节请移步:
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
全部小写化,移除空白字符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及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~