SwiftUI学习100天(Day50 - 项目 10,第二部分)

原创链接:https://www.hackingwithswift.com/100/swiftui
以下内容仅供学习参考:

今天我们将为我们的应用程序构建用户界面——除了我们处理网络的部分之外的所有内容。
虽然今天的工作基础对你来说很熟悉,但你仍然可以看到新事物的空间。随着我们继续突破 SwiftUI 的界限,这将变得尤为普遍——当你的应用程序简单时,一切都变得简单,但随着我们更多地涉足更大的应用程序,你会发现我们需要花更多时间来正确处理细节。
但没关系。正如美国轮胎大亨哈维费尔斯通曾经说过的那样,“成功是细节的总和”。我希望你能看看 Apple 的 iOS 应用程序并从中得到启发:他们的用户界面通常并不复杂,但他们投入了大量工作来确保细节正确无误,因此整个体验感觉很棒。
当用户在他们价值 1000 美元的 iPhone 上启动你的应用程序时,它会占据整个屏幕。你欠他们,也欠你自己,以确保你已尽最大努力让事情尽可能顺利地进行。苹果能做到,我们也能!
今天你要完成三个主题,在这些主题中你将使用属性观察器、观察对象、disabled()
等等。

获取基本的订单详细信息
该项目的第一步是创建一个订购屏幕,显示订单的基本细节:他们想要多少纸杯蛋糕,想要什么类型,以及是否有任何特殊定制。
在进入 UI 之前,我们需要先定义数据模型。之前我们已经使用简单的@State
值类型和@StateObject
引用类型,并且我们已经研究了如何让一个ObservableObject
包含结构的类成为可能,以便我们获得两者的好处。
在这里我们将采用不同的解决方案:我们将有一个单一的类来存储我们所有的数据,这些数据将从一个屏幕传递到另一个屏幕。这意味着我们应用程序中的所有屏幕都共享相同的数据,正如你将看到的那样,它会很好地工作。
现在这个类不需要很多属性:
蛋糕的类型,加上所有可能选项的静态数组。
用户想要订购多少蛋糕。
用户是否想要提出特殊请求,这将在我们的 UI 中显示或隐藏额外选项。
用户是否想要在蛋糕上加糖霜。
用户是否想在蛋糕上撒些糖屑。
其中每一个都需要在更改时更新 UI,这意味着我们需要用 @Published
标记它们
并使整个类符合ObservableObject
.
因此,请创建一个名为 Order.swift 的新 Swift 文件,将其 Foundation 导入更改为 SwiftUI,并为其提供以下代码:
我们现在可以通过ContentView
添加此属性在内部创建它的单个实例:
这是唯一创建订单的地方——我们应用程序中的每个其他屏幕都将传递该属性,因此它们都使用相同的数据。
我们将分三部分构建此屏幕的 UI,从纸杯蛋糕类型和数量开始。第一部分将显示一个选择器,让用户从香草、草莓、巧克力和彩虹蛋糕中进行选择,然后是一个范围为 3 到 20 的步进器来选择数量。所有这些都将被包装在一个表单中,该表单本身位于一个导航视图中,因此我们可以设置一个标题。
这里有一个小的减速带:我们的蛋糕顶部列表是一个字符串数组,但我们将用户的选择存储为一个整数——我们如何匹配两者?一个简单的解决方案是使用indices
数组的属性,它为我们提供了每个项目的位置,然后我们可以将其用作数组索引。这对于可变数组来说是个坏主意,因为数组的顺序可以随时更改,但这里我们的数组顺序永远不会更改,所以它是安全的。
现在将其放入ContentView
的正文中:
我们表单的第二部分将包含三个分别绑定到specialRequestEnabled
、extraFrosting
和addSprinkles
的拨动开关
。然而,第二个和第三个开关应该只在第一个启用时可见,所以我们将在一个条件下包装。
现在添加第二部分:
继续并再次运行该应用程序,并尝试一下——注意我是如何将第一个开关与animation()
附加的修改器绑定在一起的,这样第二个和第三个开关就可以顺利地滑入和滑出。
然而,还有另一个错误,这次是我们自己造成的:如果我们启用特殊请求,然后启用“额外糖霜”和“额外洒水”中的一个或两个,然后禁用特殊请求,我们之前的特殊请求选择保持活动状态. 这意味着如果我们重新启用特殊请求,之前的特殊请求仍然有效。
如果你的代码的每一层都知道这种问题并不难解决 – 如果当specialRequestEnabled
设置为 false时,应用程序、你的服务器、你的数据库等都被编程为忽略值extraFrosting
和addSprinkles
。然而,一个更好的想法——一个更安全的想法——是确保在specialRequestEnabled
设置为 false 时将extraFrosting
和addSprinkles
都重置为false。
我们可以通过将didSet
属性观察器添加到specialRequestEnabled
. 现在添加:
我们的第三部分是最简单的,因为它只是NavigationLink
指向下一个屏幕。我们没有第二个屏幕,但我们可以足够快地添加它:创建一个名为“AddressView”的新 SwiftUI 视图,并为其提供一个order
观察对象属性,如下所示:
我们很快就会使它更有用,但现在这意味着我们可以返回 ContentView.swift 并为我们的表单添加最后一部分。这将创建一个NavigationLink
指向 AddressView
,传入当前订单对象。
请现在添加最后一部分:
这完成了我们的第一个屏幕,所以在我们继续之前最后一次尝试一下 - 你应该能够选择你的蛋糕类型,选择一个数量,然后切换所有开关就好了。



检查有效地址
我们项目的第二步是让用户将他们的地址输入到表单中,但作为其中的一部分,我们将添加一些验证——如果他们的地址看起来不错,我们直接继续第三步。
我们可以通过向之前创建Form
的结构添加一个视图来完成此操作,该AddressView
结构体将包含四个文本字段:名称、街道地址、城市和邮编。然后我们可以添加一个NavigationLink
移动到下一个屏幕,用户将在该屏幕上看到他们的最终价格并可以结帐。
为了使这更容易理解,我们将首先添加一个名为 CheckoutView
的新视图
,一旦用户准备好,地址视图将推送到该视图。这只是避免了我们现在必须放置一个占位符然后记得稍后再回来。
因此,创建一个名为的新 SwiftUI 视图CheckoutView
,并为其提供相同的Order
观察对象属性和预览,AddressView
具有:
同样,我们稍后会再谈这个,但首先让我们实现AddressView
. 就像我说的,这需要有一个表单,其中有四个文本字段绑定到我们Order
对象的四个属性,加上一个NavigationLink
传递控制到我们的签出视图。
首先,我们需要四个新@Published
属性来存储Order
交货细节:
现在用这个替换现有body
的AddressView
:
如你所见,这将我们的order
对象传递到更深一层,到CheckoutView
,这意味着我们现在有三个视图指向相同的数据。
继续并再次运行应用程序,因为我想让你明白为什么这一切都很重要。在第一个屏幕上输入一些数据,在第二个屏幕上输入一些数据,然后尝试导航回到开头然后前进到结尾——也就是说,回到第一个屏幕,然后点击底部按钮两次以进入结帐再次查看。
你应该看到的是,无论你在哪个屏幕上,你输入的所有数据都会保存下来。是的,这是为我们的数据使用类的自然副作用,但它是我们应用程序中的一个即时功能,无需执行任何操作——如果我们使用了结构体,那么如果我们移动,我们输入的任何地址详细信息都会消失回到原来的视图。如果你真的想为你的数据使用一个结构,你应该遵循我们在项目 7 中使用的相同的结构内部类方法;当你评估你的选择时,记住这一点当然是值得的。
现在AddressView
可以了,是时候阻止用户进行结帐了,除非满足某些条件。什么条件?嗯,这取决于我们。虽然我们可以为四个文本字段中的每一个都编写长度检查,但这常常会让人们感到困惑——有些名字只有四个或五个字母,所以如果你尝试添加长度验证,你可能会不小心将人排除在外。
因此,我们只是要检查订单的name
、streetAddress
、city
和zip
属性是否为空。我更喜欢在我的数据中添加这种复杂的检查,这意味着你需要添加一个新的计算属性给Order
,
像这样:
我们现在可以将该条件与 SwiftUI 的disabled()
修饰符结合使用——将其附加到任何视图以及要检查的条件,如果条件为真,视图将停止响应用户交互。
在我们的例子中,我们要检查的条件是我们刚刚编写的计算属性,hasValidAddress
。如果那是错误的,那么包含我们的表单部分NavigationLink
应该被禁用,因为我们需要用户先填写他们的送货详细信息。
因此,将此修饰符添加到第二部分的末尾AddressView
:
代码应如下所示:
现在,如果你运行该应用程序,你会看到所有四个地址字段都必须至少包含一个字符才能继续。更好的是,SwiftUI 会在条件不为真时自动使按钮变灰,从而在交互和非交互时为用户提供真正清晰的反馈。



准备结帐
我们应用程序的最后一个屏幕是CheckoutView
,它实际上是一个分为两半的故事:前半部分是基本的用户界面,应该不会给你带来真正的挑战;但下半部分是全新的:我们需要将我们的Order
类编码为 JSON,通过 Internet 发送它,并获得响应。
我们将尽快查看整个编码和传输工作块,但首先让我们解决简单的部分:提供CheckoutView
用户界面。更具体地说,我们将创建一个ScrollView
带有图像、他们的订单总价和一个下订单按钮来启动网络。
对于图像,我已经将纸杯蛋糕图像上传到我的服务器,我们将使用AsyncImage
进行远程加载
——我们可以将其存储在应用程序中,但拥有远程图像意味着我们可以动态地将其切换为季节性替代品和促销活动。
至于订单成本,我们的数据中实际上没有任何纸杯蛋糕的定价,所以我们可以发明一个 - 这并不是说我们真的要在这里向人们收费。我们将使用的定价如下:
每个纸杯蛋糕的基本成本为 2 美元。
我们会为更复杂的蛋糕增加一点成本。
每个蛋糕额外的糖霜费用为 1 美元。
添加洒将是每个蛋糕另外 50 美分。
我们可以将所有逻辑包装在一个新的Order
计算属性中
,如下所示:
实际视图本身很简单:我们将VStack
在垂直视图中使用一个ScrollView
,然后是我们的图像、成本文本和下订单按钮。
我们将在一分钟内填写按钮的操作,但首先让我们完成基本布局——用这个替换现有CheckoutView
:的
body
到现在为止,这对你来说应该都是旧新闻了。但接下来是棘手的部分……


