项目简介
在这个项目中,我们将构建一个可在餐厅用餐后使用的账单拆分应用程序-输入食品费用,选择要留下的小费以及多少人,它将告诉您每个人需要支付的费用。
这个项目的目的不是要构建任何复杂的项目,因为它的真正目的是通过有用的方式向您介绍SwiftUI的基础知识,同时还为您提供了一个现实世界的项目,您可以根据需要进一步扩展。
您将学习UI设计的基础知识,如何让用户输入值并从选项中进行选择以及如何跟踪程序状态。由于这是第一个项目,因此我们会顺其自然,缓慢并解释所有过程——后续项目将缓慢提高速度,但现在我们将其轻松进行。
与所有涉及构建完整应用程序的项目一样,该项目分为三个阶段:
- 1、对你将要学习的所有技术的介
- 2、有关构建项目的分步指南。
- 3、您需要自己完成挑战,才能进一步推进项目。
每一项都很重要,因此不要试图匆匆跳过。
在第一步中,我将单独教给您每个单独的新组件,以便您了解它们如何单独工作。将有很多代码,但也有一些解释,因此您可以看到一切如何单独工作。此步骤是一个概述:这是我们将要使用的东西,这是它们的工作方式,这是您如何使用它们。
在第二步中,我们将采用这些概念并将其应用于实际项目中。在这里,您将看到事物在实践中是如何工作的,但是您还将获得更多的背景信息——这就是我们要使用它们的原因,这是它们与其他组件配合的方式。
在最后一步,我们将总结您学到的知识,然后对您进行简短的测试,以确保您真正了解其中的内容。您还将面临三个挑战:完成自己需要完成的三个全新任务,以确保您能够运用所学的技能。我们不提供针对这些挑战的解决方案,因为它们是在那里对您进行测试,而不是给您提供解决方案。
无论如何,说的足够多了:现在是时候开始第一个项目了。我们将研究构建账单共享应用程序所需的技术,然后在实际项目中使用这些技术。
因此,现在启动Xcode,然后选择Create A New Xcode Project。您会看到一个选项列表,我希望您选择iOS和Single View App,然后按下一步。在随后的屏幕上,您需要执行以下操作:
- Product Name,请输入“ WeSplit”。
- Organization Identifier,您可以输入所需的任何内容,但是如果您有网站,则应输入与之相反的组件:“ hackingwithswift.com”将是“ com.hackingwithswift”。如果您没有域名,请补上一个域名——"me.yourlastname.yourfirstname''很好。
- Language,请确保已选择Swift。
- User Interface,请选择SwiftUI。
- 确保未选中底部的所有复选框。
如果您对组织标识符(organization identifier)感到好奇,则应查看下面的文本:“Bundle Identifier”。苹果需要确保所有应用程序都可以唯一标识,因此它将组织标识符——您的反向网站域名——与项目名称结合在一起。因此,Apple可能具有组织标识符“com.apple”,因此Apple的Keynote应用可能具有捆绑标识符“ com.apple.keynote”。
准备就绪后,单击“下一步”,然后选择保存项目的位置并单击“创建”。Xcode会考虑一两秒钟,然后创建您的项目并打开一些代码供您编辑。
稍后,我们将使用该项目来构建我们的账单拆分应用程序,但是现在,我们将其用作沙盒,可以在其中尝试一些代码。
Let’s get to it!
- 解SwiftUI APP的基本结构
- 创建表单
- 添加导航栏
- 修改程序状态
- 将状态绑定到UI控件
- 在循环中创建视图
使用TextField读取用户的输入
我们正在构建一个账单分割应用程序,这意味着用户需要能够输入他们的账单花费,多少人分担,以及他们想留下多少小费。
希望您已经可以看到这意味着我们需要添加三个@State
属性,因为我们希望用户在我们的应用程序中输入三个数据。
因此,首先将这三个属性添加到ContentView
结构体中:
@State private var checkAmount = ""
@State private var numberOfPeople = 2
@State private var tipPercentage = 2
如您所见,这为账单金额提供了空字符串,为人数提供了默认值2,为小费百分比提供了默认值2。
你可能想知道为什么我们要用字符串来表示账单金额,而显然Int
或Double
更有效。原因是我们别无选择:SwiftUI必须使用字符串来存储TextField输入值。
有两个人作为账单支票的默认值是明智的——这在很多时候是不对的,但这仍然是一个很好的默认值。但小费比例为2似乎有些奇怪:我们真的打算留下2%的小费吗?
好吧,不是的。我们在这里使用2,因为我们将使用它从预先确定的小费值数组中取值。
我们需要将可能的小费大小列表存储在某处,因此请在前三个属性的下面添加第四个属性:
let tipPercentages = [10, 15, 20, 25, 0]
现在你可以看到小费百分比2实际上意味着20%的小费,因为这是tipPercentages[2]
的值。
将body属性修改为:
var body: some View {
Form {
Section {
TextField("Amount", text: $checkAmount)
}
}
}
这将创建一个滚动条目形式的一个部分,其中又包含一行:我们的文本输入框。在表单中创建文本输入框时,第一个参数是用作占位符的字符串——文本字段旁边显示的灰色文本,让用户了解其中应包含的内容。第二个参数是到checkAmount
属性的双向绑定,这意味着该属性将被更新。
@State
属性包装器的一个优点是,它自动监视更改,当发生某些事情时,它将自动重新调用body属性。这是一种奇特的说法,它将重新加载您的UI以反映更改后的状态,这是SwiftUI工作方式的一个基本特性。
要演示这一点,请添加第二个部分,其中包含显示checkAmount
值的文本视图,如下所示:
Form {
Section {
TextField("Amount", text: $checkAmount)
}
Section {
Text("$\(checkAmount)")
}
}
稍后我们将制作其他内容,但现在请在模拟器中运行该应用程序,以便您可以自己尝试。
点击“账单金额”文本输入框,然后输入一些文本——它不需要是数字;任何事情都可以。您将看到,当您在第二部分中键入文本视图时,它会自动并立即反映您的操作。
发生此同步的原因是:
1.我们的文本输入框与
checkAmount
属性有双向绑定。
2.checkAmount
属性用@State
标记,它自动监视值的更改。
3.当@State
属性更改时,SwiftUI将重新调用body
属性(即,重新加载我们的UI)
4.因此,文本视图将获得checkAmount
的更新值。
最终的项目不会在文本视图中显示checkAmount
,但现在已经足够了。不过,在我们继续之前,我想解决一个重要的问题:当您点击在文本输入框中输入文本时,用户会看到一个普通的字母键盘。当然,他们可以点击键盘上的一个按钮进入数字屏幕,但这很烦人,也不是很有必要。
幸运的是,文本字段有一个修饰符,允许我们强制使用另一种键盘:keyboardType()
。我们可以给它一个参数来指定我们想要的键盘类型,在这个例子中,.numberPad
或.decimalPad
都是不错的选择。这两个键盘都会显示数字0到9供用户点击,但decimalPad
也会显示小数点,这样用户就可以输入32.50美元之类的账单金额,而不仅仅是整数。
所以修改TextField如下:
TextField("Amount", text: $checkAmount)
.keyboardType(.decimalPad)
你会注意到keyboardType
之前添加了一个换行符,并且将它缩进了比TextField
深一层的地方——这不是必需的,但是它可以帮助你跟踪哪些修改器应用于哪些视图。
PS: 可以选中代码 Ctrol + i ,自动对齐代码
现在继续运行应用程序,你会发现你现在只能在文本输入框中键入数字。
提示:.numberPad
和.decimalPad
键盘类型告诉SwiftUI显示数字0到9以及可选的小数点,但这并不阻止用户输入其他值。例如,如果他们有一个硬件键盘,他们可以键入自己喜欢的内容,如果他们从其他地方复制了一些文本,他们就可以将其粘贴到文本字段中,无论文本中包含什么内容。不过,没关系,我们稍后会处理这件事。
在表单中创建选择器
SwiftUI的Picker
有多种用途,具体的外观取决于您使用的设备和Picker
所在的上下文。
在我们的项目中,我们有一个表单,要求用户输入他们的账单金额,我们希望添加一个选择器,以便他们可以选择有多少人将共享支票。
选择器与文本输入框一样,需要对属性进行双向绑定,以便跟踪其值。为此,我们已经创建了一个名为numberOfPeople
的@State
属性,因此我们的下一个任务是循环遍历从2到99的所有数字,并在选择器中显示它们。
修改表单中的第一个部分以包含选择器,如下所示:
Section {
TextField("Amount", text: $checkAmount)
.keyboardType(.decimalPad)
Picker("Number of people", selection: $numberOfPeople) {
ForEach(2 ..< 100) {
Text("\($0) people")
}
}
}
现在在模拟器中运行这个程序并尝试它-你注意到了什么?
希望你能发现以下几点:
1、新的一行写着“Number of people”在左边,而“4 people”在右边。
2、右边有一个灰色的指示器,这是iOS发出的信号,点击该行将显示另一个屏幕。
3、点击该行不会显示另一个屏幕。
4、行中显示“4 people”,但我们给numberOfPeople
属性的默认值为2。
所以,这有点“前进两步,后退两步(two steps forward, two steps back)”——我们有一个不错的结果,但它不起作用,也没有显示正确的信息!
我们将修复这两个问题,首先从简单的问题开始:当我们给numberOfPeople
默认值2时,为什么它会说4个人?在创建Picker
时,我们使用了如下ForEach
视图:
ForEach(2 ..< 100) {
从2到100,创建行。这意味着我们的第0行(创建的第一行)包含“2个人”,所以当我们给numberOfPeople
设置的值为2时,实际上是将它设置为第三行,即“4个人”。
所以,尽管这有点让人头疼,但我们的用户界面显示的是“4个人”而不是“2个人”这一事实并不是一个错误。但是在我们的代码中仍然有一个很大的错误:为什么点击这一行什么也不做?
如果你自己在Form
之外创建一个选择器,iOS将显示一个可选的旋转轮。不过,在这里,我们告诉SwiftUI这是一个用户输入表单,因此它自动改变了选择器的外观,这样就不会占用太多空间。
SwiftUI想要做的是显示一个新的视图,其中包含我们选择器的选项,这也是为什么它在行的右边缘添加了灰色显示指示器的原因。要做到这一点,我们需要添加一个导航视图,它可以做两件事:在顶部给我们一些空间来放置标题,还可以根据需要让iOS在新视图中滑动。
所以,直接在表单前面添加NavigationView{
,在表单的右大括号后面添加另一个右大括号。如果正确,代码应该如下所示(这个时候,Command + a,再 Control + i,你会发现奇迹):
var body: some View {
NavigationView {
Form {
// everything inside your form
}
}
}
如果你再次运行这个程序,你会在顶部看到一个很大的灰色空间,这就是iOS给我们放置标题的空间。稍后我们会这样做,但首先尝试点击人数行,您应该会看到滑入一个新的屏幕与所有其他可能的选择。
您应该看到“4个人”旁边有一个复选标记,因为它是所选的值,但是您也可以点击不同的数字——屏幕将自动再次滑动,将用户带着新的选择返回到上一个屏幕。
您在这里看到的是所谓的声明式用户界面设计的重要性。这意味着我们说我们想要什么,而不是说应该怎么做。我们说我们想要一个里面有一些值的选择器,但是由SwiftUI决定是使用轮子选择器还是滑动视图方法更好。它之所以选择滑动视图方法,是因为选择器位于表单内部,但在其他平台和环境中,它可以选择其他方法。
在完成此步骤之前,让我们向新的导航栏添加一个标题。给表单这个修饰语:
.navigationBarTitle("WeSplit")
提示:很容易认为应该将修饰符附加到NavigationView
的末尾,但是需要将其附加到表单的末尾。原因是导航视图能够在程序运行时显示许多视图,因此通过将标题附加到导航视图中的内容,我们允许iOS自由更改标题。
使用分段控件选择百分比
现在我们要在我们的应用程序中添加第二个Picker
视图,但这次我们想要的是稍微不同的东西:我们想要一个分段控件(segmented control)。这是一种特殊的选择器,它在水平列表中显示少数选项,当您只有一小部分选项可供选择时,它工作得很好。
我们的表单已经有两个部分:一个是数量和人数,另一个是我们将显示最终结果的部分——它只是暂时显示checkAmount
,但我们将很快修复它。
在这两个部分中间,我希望您添加第三个部分以显示小费百分比:
Section {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(0 ..< tipPercentages.count) {
Text("\(self.tipPercentages[$0])%")
}
}
}
这将计算tipPercentages
数组中的所有选项,并将每个选项转换为文本视图。就像前面的选择器一样,SwiftUI会将其转换为列表中的一行,并在点击时滑入一个新的选项屏幕。
不过,在这里,我想告诉您如何使用分段控件,因为它看起来好多了。因此,请将此修饰符添加到小费选择器:
.pickerStyle(SegmentedPickerStyle())
应该在选择器的右大括号的末尾,如下所示:
Section {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(0 ..< tipPercentages.count) {
Text("\(self.tipPercentages[$0])%")
}
}
.pickerStyle(SegmentedPickerStyle())
}
如果你现在运行这个程序,你会发现事情开始变得复杂起来:用户现在可以在他们的账单上输入金额,选择人数,然后选择他们想留下多少小费——还不错!
但事情并不像你想的那样。应用程序开发人员面临的一个问题是,我们理所当然地认为,我们的应用程序做了我们想做的事情——我们设计它是为了解决一个特定的问题,所以我们自动知道一切意味着什么。
如果可以的话,试着用新的眼光看一下我们的用户界面:
- “Amount”是有意义的——用户可以在其中键入数字。
- “Number of people”也可以自我注释。
- 底部的标签是我们显示总数的地方,所以现在我们可以忽略它。
- 不过,中间那部分——这些百分比是什么意思?
是的,我们知道这是他们要选择小费的多少,但这在屏幕上并不明显。我们可以而且应该做得更好。
一个选项是直接在分段控件之前添加另一个文本视图,我们可以这样做:
Section {
Text("How much tip do you want to leave?")
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(0 ..< tipPercentages.count) {
Text("\(self.tipPercentages[$0])%")
}
}
.pickerStyle(SegmentedPickerStyle())
}
这可以完成工作,但看起来不太好——它看起来像是一个单独的项,而不是分段控件的标签。
更好的方法是修改Section
本身:SwiftUI允许我们向Section
的页眉和页脚添加视图,在本例中,我们可以使用它添加一个小的解释性提示。实际上,我们可以使用刚刚创建的文本视图,只是上移到 Section
标题,而不是其中的松散标签。
代码应该长得像下面这样:
Section(header: Text("How much tip do you want to leave?")) {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(0 ..< tipPercentages.count) {
Text("\(self.tipPercentages[$0])%")
}
}
.pickerStyle(SegmentedPickerStyle())
}
这是对代码的一个小改动,但我认为最终结果看起来好多了——文本现在看起来像是位于它下面的分段控件的提示。
计算每个人的金额
到目前为止,最后一部分显示了一个简单的文本视图,其中包含用户输入的任何账单金额,但现在是该项目重要部分的时候了:我们希望该文本视图显示每个人需要为账单支付多少。
有几种方法可以解决这个问题,但最简单的方法恰好也是最干净的方法,我的意思是它给了我们清晰易懂的代码:我们将添加一个计算总数的计算属性。
这需要做少量的数学运算:每人应支付的总金额等于订单价值加小费百分比除以人数。
但在我们做这件事之前,我们首先需要找出有多少人,小费百分比是多少,以及订单的价值。听起来很简单,但有一些小问题:
- 正如您已经看到的,
numberOfPeople
被设置为了2——当它存储值3时,意味着5个人。 -
tipPercentage
整数在tipPercentages
数组中存储索引,而不是实际的tip百分比。 -
checkAmount
属性是用户输入的字符串,可能是20这样的有效数字,也可能是“fish”这样的字符串,甚至可能是空的。
所以,我们将创建一个新的计算属性totalPerPerson
,它将是一个Double
,它将从上面的三个值开始。
首先,在body
属性之前添加计算属性:
var totalPerPerson: Double {
// calculate the total per person here
return 0
}
它将返回0,这样代码就不会崩溃,但是我们将用我们的计算替换//calculate the total per person here
注释。
接下来,我们可以通过读取numberOfPeople
并添加2来计算出有多少人。记住,这个值的范围是2到100,但它是从0开始计算的,所以我们需要添加2。
所以,首先用以下内容替换//calculate the total per person here
:
let peopleCount = Double(numberOfPeople + 2)
您会注意到,将结果值转换为Double
,这样我们就可以将所有内容相加并将其除以,而不会失去准确性。
接下来我们需要计算出实际的小费百分比。我们的tipPercentage
属性存储用户选择的值,但实际上这只是tipPercentages
数组中的一个索引。因此,我们需要查看tipPercentages
以确定他们选择了什么,然后再次将其转换为Double
,这样我们就可以保持所有精度——将其添加到前一行下面:
let tipSelection = Double(tipPercentages[tipPercentage])
我们需要计算的最后一个数字是他们账单的金额。现在,如果您还记得这实际上是一个字符串,因为它被用作对文本输入框的双向绑定。尽管我们编写代码只显示十进制键盘,但没有什么可以阻止创造性用户在其中输入无效值,因此我们需要小心处理。
我们想要的另一个Double
是账单金额。实际上,我们有一个字符串可能包含也可能不包含有效的Double
:它可能是22.50,可能是空字符串,也可能是莎士比亚的全部作品。它是一根绳子,几乎可以是任何东西。
幸运的是,Swift有一个将字符串转换为Double
的简单方法,它看起来如下:
let stringValue = "0.5"
let doubleValue = Double(stringValue)
这看起来很简单,但有一个问题:doubleValue
的类型最终是Double?
而不是Double
——是的,这是一个可选的。你看,Swift不能确定字符串是否包含可以安全地转换为Double
的内容,所以它使用可选值:如果转换成功,那么我们的optional将包含结果值,但是如果字符串是无效的(“Fish”,莎士比亚的全集,etc)则可选项将设置为nil
。
这里有几种处理可选性的方法,但最简单的方法是使用空合运算符??
,以确保始终存在有效值。
let orderAmount = Double(checkAmount) ?? 0
它将尝试将checkAmount
转换为Double
,但如果由于某种原因失败,则将使用0。
现在我们有了三个输入值,是时候做我们的数学题了。这还需要三个步骤:
1、我们可以通过将
orderAmount
除以100并乘以tipSelection
来计算tip值。
2、我们可以通过向orderAmount
添加tip值来计算账单的总金额。
3、我们可以用总金额除以人数来计算出每人的金额。
一旦完成,我们可以返回每人的金额,我们就完成了。
将属性中的return 0
替换为:
let tipValue = orderAmount / 100 * tipSelection
let grandTotal = orderAmount + tipValue
let amountPerPerson = grandTotal / peopleCount
return amountPerPerson
如果您正确地执行了所有操作,那么您的代码应该如下所示:
var totalPerPerson: Double {
let peopleCount = Double(numberOfPeople + 2)
let tipSelection = Double(tipPercentages[tipPercentage])
let orderAmount = Double(checkAmount) ?? 0
let tipValue = orderAmount / 100 * tipSelection
let grandTotal = orderAmount + tipValue
let amountPerPerson = grandTotal / peopleCount
return amountPerPerson
}
既然totalPerPerson
给出了正确的值,我们可以更改表中的最后一部分,以便它显示正确的文本。
将这段代码:
Section {
Text("$\(checkAmount)")
}
替换为:
Section {
Text("$\(totalPerPerson)")
}
现在试着运行应用程序,看看你怎么想。您应该会发现,因为构成总数的所有值都用@State
标记,更改其中任何一个值都会导致总数自动重新计算。
希望您现在可以亲眼看到,SwiftUI的视图是其状态的函数——当状态改变时,视图会自动更新以匹配。
在我们完成之前,我们要解决显示的一个小问题,这就是总价格的显示方式。我们的金额计算使用了双精度,这意味着Swift给我们的精度比我们需要的要高得多——我们预计会看到25.50美元,但实际上是25.500000美元。
我们可以通过使用SwiftUI添加的一个简洁的字符串插值功能来解决这个问题:决定数字应该如何在字符串中格式化的能力。这实际上可以追溯到C编程语言,所以语法一开始有点奇怪:我们编写一个名为specifier
的字符串,给它值“%.2f”。这是C的语法,意思是“两位浮点数”
非常粗略地说,“%f”意味着“任何类型的浮点数”,在我们的例子中,它将是整个数字。另一个选择是“%g”,它也做同样的事情,只是它从末尾去掉了不重要的零——12.50美元将被写成12.5美元。把“.2”放进混合物中,就是要求小数点后有两位数字,不管它们是什么。Swift足够聪明地绕过它们,所以我们仍然可以得到很好的精度。
你可以在Wikipedia上阅读更多关于这些C-style格式说明符的信息:https://en.wikipedia.org/wiki/Printf_format_string——我们不会去其他任何地方,所以如果你想满足你的好奇心,它就在那里!
无论如何,我们希望每人的金额使用新的格式说明符,因此请将总金额文本视图修改为:
Text("$\(totalPerPerson, specifier: "%.2f")")
现在最后一次运行这个项目——我们完成了!
项目打卡
译自 Hacking with iOS: SwiftUI Edition - WeSplit
WeSplit: Introduction
reading text from the user with textfield
Creating pickers in a form
Adding a segmented control for tip percentages
Calculating the total per person
Previous: 在循环中创建视图 | Hacking with iOS: SwiftUI Edition | Next: SWeSplit 挑战 |
---|
赏我一个赞吧~~~