[SwiftUI练级-2] 餐费计算器 · part2
更多内容欢迎关注公众号:BoBo清吧
第一天我们了解构建这个餐费计算器app需要用到的技术,是时候把这些知识转化成一个实际的app了。
SwiftUI 的一大优点是,从理论到实践的转换很直接。
当然,某些情况下我会留上一两手,以便你提起注意力。不过要完成这个项目的大多数知识,你已经有所了解。现在让我们把这些碎片组装起来。
接下来一共有四个主题,你将应用前面学到的有关Form,@State,Picker的知识。
用文本框从用户那里读取文本
我们在构建的是一个AA计算器app,意味着用户需要输入账单的总金额,参与AA的人数,以及他们想要留下的小费数额。
希望你已经想到,我们需要用到三个@State属性,因为有三块数据是我们需要用户输入app的。
那就从添加三个 @State 属性到ContentView结构体开始吧:
@State private var checkAmount =""@State private var numberOfPeople = 2@State private var tipPercentage = 2
如你所见,上面的代码给账单金额一个空的字符串,给总人数默认值2,给小费比例默认值2。
你一定会好奇,为什么账单金额要用字符串来表示呢?很显然Int或者Double不是更合理吗?这个嘛,原因在于我们别无选择:SwiftUI只允许用字符串来存储文本框的值。
AA的人数默认取2也是合理的 —— 虽然大多数时候不一定正确,但它是一个好的默认值。那2%的小费比例呢?看起来很奇怪是不是?我们一般不会留 2% 的小费吧?
是的,当然不会。但这里的2并不直接代表百分比,我们这里会用它来从一组预先定义好的小费比例的数组中选取一个数值。你将会看到不同类型的 Picker 如何工作。
因为我们需要存储所有可能的小费比例,让我们创建一个新的属性:
lettipPercentages = [10, 15, 20, 25, 0]
现在你明白2实际上代表 20% 的小费,因为它是tipPercentages[2]的值。
我们将一步一步地构建这个app,首先从用户输入账单金额的文本框开始。
把body属性修改成这样:
var body: some View { Form { Section { TextField("Amount", text:$checkAmount) } }}
上面的代码会创建一个section,它包含一行:我们的文本框。当你在表单中创建文本框的时候,第一个参数是字符串,它被用作占位符——文本框里显示的灰色文本,告知用户文本框里应该填写什么内容。第二个参数是对checkAmount属性的双向绑定,这意味着当用户输入时,属性也将被更新。
@State属性包装器有一个极大的好处是:它会自动关注属性的变化。当改变发生时,body计算属性会被自动调用。换句话说,你的UI 会被重新加载,以反映改变的状态,而这正是 SwiftUI 工作方式的一个基础特性。
为了演示这一点,我们添加第二个 Section,展示checkAmount的值,就像这样:
Form { Section { TextField("Amount", text:$checkAmount) } Section { Text("$\(checkAmount)") }}
稍后我们会让它再显示别的东西,现在让我们在模拟器里运行这个app尝试一下吧。
点击账单金额的文本框,输入文字——不需要一定是数字,任何文字都可以。你会发现随着你的输入,第二个 section 会自动更新,反映你的动作。
这个同步过程之所以会发生是因为:
我们的文本框有一个队checkAmount属性的双向绑定。
checkAmount属性被标记了@State,它会自动关注属性值的变化。
当一个@State属性发生改变时,SwiftUI 将重新调用body属性 (也就是重新加载我们的 UI)。
因此第二静态文本会自动获得更新后的checkAmount的值。
最终的项目并不会在那个文本视图里展示checkAmount,但目前为止这样足够了。在我们推进之前,我还想指出一个重要的问题:当你点击文本框输入文本时,用户看到的是一个常规的字母键盘。当然,他们可以点击键盘上的按钮切换到数字键盘,但这会变得很烦人并且没有必要。
幸运的是,文本框有一个修改器,允许我们强制不同的键盘类型:keyboardType()。我们可以给这个参数指定我们想要的键盘类型,在这例子中,.numberPad或者.decimalPad都是不错的选择。两种键盘都会显示数字0 到数字 9,但是.decimalPad也会显示小数点。因此,如果用户可以输入像 ¥120.50 这样的数字而非全部都是整数。
修改文本框:
TextField("Amount", text:$checkAmount) .keyboardType(.decimalPad)
你注意到我在.keyboardType前面加了一个换行,并且有意地相对TextField做了一级缩进。这并非要求的,但可以帮助阅读,以一眼看清修改器是应用于哪些视图。
再次运行app,这次你会发现你可以直接输入数字到文本框里了。
Tip:.numberPad和.decimalPad键盘类型告诉 SwiftUI 展示数字 0 到数字 9 以及可选的小数点,但这并未阻止用户输入其他类型的值。举个例子,如果用户有实体键盘他们可以输入他们想要的文本,或者他们从别的某个地方复制了文本并且粘贴进文本框。没关系,到最后我们会处理这个问题。
在表格里创建Picker
SwiftUI 的 Picker 服务于几种目的,它们长啥样是取决具体的设备以及它们所处的上下文。
在我们的项目中,我们有一个表单,要求用户输入账单总金额,然后我们会添加一个 Picker,以便用户可以选择总共有几个人要参与分摊费用。
Picker,就像文本框,也需要一个基于属性的双向绑定,以便追踪一个值。前面我们已经创建了一个@State属性服务于这个控件,名字叫numberOfPeople,因此我们接下来的任务是遍历2到99,然后在 Picker 中显示它们。
修改表单中的第一个Section,改成这样:
Section { TextField("Amount", text:$checkAmount) .keyboardType(.decimalPad) Picker("Number of people", selection:$numberOfPeople) { ForEach(2 ..< 100) { Text("\($0) people") } }}
在模拟器里再次尝试以上代码。
希望你注意到几件事:
表单里出现一个新行,左边是 “Number of people” ,右边是 “4 people”。
右边缘有一个灰色的指示器,它是iOS用以表示点击该行会跳转一个新界面的方式。
点击该行并没有跳转新界面。
该行显示的是 “4 people”,但我们给numberOfPeople属性的默认值是 2。
我们会修复上面的问题,先来个简单的吧:为什么是4个人而不是numberOfPeople的默认值2呢?注意,用ForEach创建视图时的代码是这样的:
ForEach(2 ..< 100) {复制代码
计数是从 2 到 100,创建行。这意味着我们的第0行 —— 被创建的第一行,包含的是 “2 People”,因此当我们给numberOfPeople设置值为 2 时,我们实际上是在选取第3行,也就是 “4 People”。
所以,尽管说起来有点绕,我们的 UI 显示 “4 people” 而不是 “2 people” 其实并不是一个bug。那么我们的代码中就剩一个大bug了:为什么点击这一行没有反应呢?
如果你是表单之外创建 picker,iOS 会显示 spinning wheel 风格的 picker。但是在这里,我们已经告知 SwiftUI 这是一个供用户输入的表单,因此 picker 的外观会自动改变,以便尽量少占空间。
现在,让我们添加一个导航视图,它能为我们做两件事:一是在顶部提供一块区域放置标题,二是让iOS在需要时滑入新的视图。
#代码如下:
var body: some View { NavigationView { Form { Section { TextField("Amount", text:$checkAmount) .keyboardType(.decimalPad) Picker("Number of people", selection:$numberOfPeople) { ForEach(2 ..< 100) { Text("\($0) people") } } } Section { Text("$\(checkAmount)") } } }}
再次运行程序,你会看到顶部区域现在有一大片灰色区域,这是iOS提供给我们放置标题的空间。我们稍后会添加标题,现在让我们先点击 Number Of People 这一行,你会发现新的一屏滑入,包含所有 Picker 的选项。
你会发现 “4 People” 的旁边有一个选中标记。因为它是被选中的值,当你点击其他的选项,屏幕会自动滑回前一屏,选项变成你刚才选择的新值。
到这里,你应该对”声明式UI设计“的重要性有所体会。声明式意味着我们说出我们想要的,而不是事情应该怎么做。我们说我们需要一个 picker ,里面包含了哪些值,但剩下的事,包括是采用舵轮还是新的视图,这是由 SwiftUI 决定的。它之所以选择了滑出新的视图,是因为 picker 处在表单内,但在其他的平台和环境下,它可能会选择别的方式。
在我们完成这一步之前,让我们给导航栏加上标题,用一个修改器来实现:
.navigationBarTitle("WeSplit")
提示:我猜你一定会忍不住把这个修改器加在NavigationView 后面对吧?但实际上它应该被加在Form的后面。原因是,NavigationView 其实可以在你的程序运行时显示许多不同的标题,这些标题是从它当下具体的子内容中获取的。你可以把它理解成视图的一个属性,NavigationView 作为视图的父节点,可以读取这些属性,并且根据上下文选择最合适的那个标题,浮动到顶部区域。