Cupcake Corner:介绍
在此项目中,我们将构建一个用于订购蛋糕的多屏应用。这将使用两种形式,这对您来说是个老新闻,但您还将学习如何在具有@Published
属性的情况下使类符合Codable
,如何从Internet发送和接收订单数据,如何验证表格等等。
随着我们继续深入研究Codable
,我希望您会继续对它的灵活性和安全性印象深刻。特别是,我希望您记住它与较旧的UserDefaults
API有很大的不同——不用担心完全正确地键入字符串,真是太好了!
无论如何,我们还有很多工作要做,让我们开始吧:使用Single View App模板创建一个新的iOS应用,并将其命名为CupcakeCorner。如果您尚未下载本书的项目文件,请立即获取它们:https://github.com/twostraws/HackingWithSwift
与往常一样,我们将从项目所需的新技术开始……
获取基本订单详细信息
该项目的第一步将是创建一个订购界面,该屏幕将接受订单的基本详细信息:他们想要多少杯形蛋糕,他们想要哪种杯形蛋糕以及是否有任何特殊的自定义设置。
在进入UI之前,我们需要先定义数据模型。以前,我们将@State
用于简单的值类型并将@ObservedObject
用于引用类型,并且我们研究了包含结构体的ObservableObject
类的实现,以便我们能从两者中受益。
在这里,我们将采用不同的解决方案:我们将拥有一个存储所有数据的类,该类将在界面之间传递。这意味着我们应用中的所有屏幕都共享相同的数据,这将如您所愿地运行得很好。
目前,该类不需要很多属性:
- 蛋糕的类型,以及所有可能选项的静态数组。
- 用户想要订购多少蛋糕。
- 用户是否要提出特殊请求,这将在我们的UI中显示或隐藏其他选项。
- 用户是否要在蛋糕上加糖霜。
- 用户是否要在蛋糕上撒些巧克力。
这些中的每一个都需要在更改时更新UI,这意味着我们需要用@Published
标记它们,并使整个类符合ObservableObject
协议。
因此,请创建一个名为Order.swift的新Swift文件,更改它的导入的 Foundation 为 SwiftUI,并为其提供以下代码:
class Order: ObservableObject {
static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]
@Published var type = 0
@Published var quantity = 3
@Published var specialRequestEnabled = false
@Published var extraFrosting = false
@Published var addSprinkles = false
}
现在,我们可以通过添加以下属性在ContentView
中创建该实例的单个实例:
@ObservedObject var order = Order()
这是创建订单的唯一位置——我们应用中的所有其他屏幕都会通过该属性传递,因此它们都可以使用相同的数据。
我们将分三部分为该屏幕构建用户界面,首先是纸杯蛋糕的类型和数量。第一部分将显示一个选择器,用户可以从香草,草莓,巧克力和彩虹蛋糕中进行选择,然后选择3到20之间的步进器来选择数量。所有这些都将包装在一个表单中,而表单本身又位于导航视图中,因此我们可以设置标题。
现在将其放入ContentView
的 body
中:
NavigationView {
Form {
Section {
Picker("Select your cake type", selection: $order.type) {
ForEach(0..
在我们添加第二部分和第三部分之前,我希望您构建并运行代码,然后尝试一下。我们的两个表单字段应该可以正常工作,但是您可能会在Xcode中看到有关ForEach
的常规烦人消息:“count (4) != its initial count (1)”。
我之所以说这样可能是因为,这感觉就像是另一个SwiftUI错误:即使我们的ForEach
使用固定数据(即Order.types
数组中的项目数),SwiftUI似乎也很难使用它。如果您没有看到该错误,则无需担心,但是如果您看到该错误,那么我想向您展示如何解决该错误:我们需要给它提供一个明确的标识符。
因此,将您的ForEach
修改为此:
ForEach(0..
可以完全解决问题,但是老实说,我希望这个错误会在以后的SwiftUI更新中消失(此错误已经修复)。
我们表单的第二部分将包含三个分别绑定到specialRequestEnabled
,extraFrosting
和addSprinkles
的切换开关。不过,只有在启用第一个和第二个开关的情况下,第二个和第三个开关才应该可见,因此我们将其包装为一个条件。
现在添加第二部分:
Section {
Toggle(isOn: $order.specialRequestEnabled.animation()) {
Text("Any special requests?")
}
if order.specialRequestEnabled {
Toggle(isOn: $order.extraFrosting) {
Text("Add extra frosting")
}
Toggle(isOn: $order.addSprinkles) {
Text("Add extra sprinkles")
}
}
}
继续并再次运行该应用程序,然后尝试一下——注意如何将第一个切换开关与附加的animation()
修饰符绑定在一起,以便第二个和第三个切换开关可以平稳地滑入和滑出。
但是,还有另一个错误,这一次是我们自己制作的:如果启用特殊请求,然后启用“extra frosting”和“extra sprinkles”中的一个或两个,然后禁用特殊请求,则我们先前的特殊请求选择将保持活动状态。这意味着,如果我们重新启用特殊请求,则先前的特殊请求仍处于活动状态。
一个安全的主意:确保将specialRequestEnabled
设置为false时将extraFrosting
和addSprinkles
都重置为false。我们可以通过将didSet
属性观察器添加到specialRequestEnabled
来实现这一点。立即添加:
@Published var specialRequestEnabled = false {
didSet {
if specialRequestEnabled == false {
extraFrosting = false
addSprinkles = false
}
}
}
我们的第三部分是最简单的,因为它将是指向下一个屏幕的NavigationLink
。我们没有第二个屏幕,但是我们可以足够快地添加它:创建一个名为“AddressView”的新SwiftUI视图,并为它提供一个顺序观察对象属性,如下所示:
struct AddressView: View {
@ObservedObject var order: Order
var body: some View {
Text("Hello World")
}
}
struct AddressView_Previews: PreviewProvider {
static var previews: some View {
AddressView(order: Order())
}
}
我们将在短期内使它更加有用,但是现在,这意味着我们可以返回 ContentView.swift 并为表单添加最后一部分。这将创建一个NavigationLink
,该URL指向一个AddressView
,并传入当前订单对象。
请现在添加最后一部分:
Section {
NavigationLink(destination: AddressView(order: order)) {
Text("Delivery details")
}
}
这样就完成了我们的第一个屏幕,因此请在上一次尝试之前继续尝试——您应该能够选择蛋糕类型,选择数量并正确切换所有开关。
检查有效地址
我们项目的第二步是让用户在表单中输入他们的地址,但是在此过程中,我们将添加一些验证——如果他们的地址看起来不错,我们将继续进行第三步。
我们可以通过将Form
视图添加到之前制作的AddressView
结构体中来完成此操作,该结构体将包含四个文本字段:名称,街道地址,城市和邮政编码。然后,我们可以添加NavigationLink
移至下一个界面,在该界面上,用户将看到其最终价格并可以付款。
为了使操作更容易进行,我们将首先添加一个名为CheckoutView
的新视图,一旦用户准备好,该地址视图就会被push到该视图。这只是避免了我们现在必须放置一个占位符,然后记得稍后再回来。
因此,创建一个名为CheckoutView
的新SwiftUI视图,并为其提供与OrderView
对象相同的Order
观察对象属性并进行预览:
struct CheckoutView: View {
@ObservedObject var order: Order
var body: some View {
Text("Hello, World!")
}
}
struct CheckoutView_Previews: PreviewProvider {
static var previews: some View {
CheckoutView(order: Order())
}
}
再说一遍,我们稍后再讲,但是首先让我们实现AddressView
。就像我说的那样,这需要具有一个表单,其中包含四个文本字段,这些文本字段绑定到我们的Order
对象的四个属性,另外还有一个NavigationLink
将控件传递给我们的付款视图。
首先,我们需要四个新的@Published
属性以存储Order
详细信息:
@Published var name = ""
@Published var streetAddress = ""
@Published var city = ""
@Published var zip = ""
现在,用以下内容替换AddressView
的现有body
:
Form {
Section {
TextField("Name", text: $order.name)
TextField("Street Address", text: $order.streetAddress)
TextField("City", text: $order.city)
TextField("Zip", text: $order.zip)
}
Section {
NavigationLink(destination: CheckoutView(order: order)) {
Text("Check out")
}
}
}
.navigationBarTitle("Delivery details", displayMode: .inline)
如您所见,这会将订单对象更深一层地传递给CheckoutView
,这意味着我们现在有三个指向相同数据的视图。
继续并再次运行该应用程序,因为我希望您了解所有这些原因的重要性。在第一个屏幕上输入一些数据,在第二个屏幕上输入一些数据,然后尝试导航回到起点,然后前进到末尾——也就是说,返回到第一个屏幕,然后单击底部的按钮两次以进入结帐再次查看。
您应该看到的是无论您在什么屏幕上,输入的所有数据都会保留下来。是的,这是对数据使用类的自然副作用,但这是我们应用程序中的即时功能,无需执行任何工作——如果我们使用了结构体,那么如果我们移走了,我们输入的任何地址详细信息都将消失回到原始视图。如果您确实想对数据使用结构体,则应遵循项目7 - iExpense中使用的相同的内部结构方法。在评估选项时,一定要牢记这一点。
现在,AddressView
可以正常工作了,除非满足某些条件,否则是时候停止用户进行结帐了。什么条件?好吧,这取决于我们来决定。尽管我们可以为四个文本字段中的每一个编写长度检查,但这经常使人们感到困惑——有些名称只有四个或五个字母,因此,如果您尝试添加长度验证,则可能会意外地排除某些用户。
因此,我们只需要检查订单的name
,streetAddress
,city
和zip
属性是否为空。我更喜欢在数据中添加这种复杂的检查,这意味着您需要像下面这样向Order
添加一个新的计算属性:
var hasValidAddress: Bool {
if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
return false
}
return true
}
现在,我们可以将该条件与SwiftUI的disabled()
修饰符一起使用——将其与要检查的条件一起附加到任何视图,如果条件为 true,则该视图将停止响应用户交互。
在我们的例子中,我们要检查的条件是我们刚刚编写的计算属性hasValidAddress
。如果那是 false,那么包含我们的NavigationLink
的表单部分应该被禁用,因为我们需要用户首先填写其交付详细信息。
因此,将此修饰符添加到AddressView
第二部分的末尾:
.disabled(order.hasValidAddress == false)
该代码应如下所示:
Section {
NavigationLink(destination: CheckoutView(order: order)) {
Text("Check out")
}
.disabled(order.hasValidAddress == false)
}
现在,如果您运行该应用程序,您将看到所有四个地址字段必须至少包含一个字符才能继续。更好的是,SwiftUI在不符合条件的情况下会自动使按钮变灰,从而在交互和不交互时为用户提供真正清晰的反馈。
准备结帐
我们应用程序的最后一个界面是CheckoutView
,这实际上是一个分成两半的故事:前半部分是基本的用户界面,应该不会给您带来什么真正的挑战。但是第二部分是全新的:我们需要将Order
类编码为JSON,通过Internet发送并获得响应。
我们将尽快研究整个编码和传输工作,但是首先让我们解决一个简单的部分:为CheckoutView
提供一个用户界面。更具体地说,我们将创建一个ScrollView
,其中包含图片,订单总价和一个下订单按钮以启动联网。
对于图像,我在该项目的文件中提供了两个图像,可从https://github.com/twostraws/HackingWithSwift获得。在SwiftUI / project10-files
文件夹中查找,然后将 [email protected]
和 [email protected]
复制到资产目录中。我们需要使用GeometryReader
将此图片调整为正确的屏幕宽度,以避免它在较小的设备上拉伸布局。
至于订单价格,我们的数据中实际上没有纸杯蛋糕的任何价格,因此我们只能发明一个——并不是说我们实际上要在这里向人们收费。我们将使用的定价如下:
- 每块纸杯蛋糕的基本价格为$2。
- 对于较复杂的蛋糕,我们会增加一些成本。
- 额外的糖霜费用为每个蛋糕$1。
- 再加上需要撒上巧克力末,每块蛋糕还要再加50美分。
我们可以将所有逻辑包装在Order
的新计算属性中,如下所示:
var cost: Double {
// 每个蛋糕 $2
var cost = Double(quantity) * 2
// 复杂的蛋糕成本更高
cost += (Double(type) / 2)
// 额外的糖霜费 $1
if extraFrosting {
cost += Double(quantity)
}
// 添加巧克力末 $0.50
if addSprinkles {
cost += Double(quantity) / 2
}
return cost
}
实际的视图本身很简单:我们将使用GeometryReader
来确保蛋糕图像的尺寸正确,并在垂直ScrollView
中使用VStack
,然后使用我们的图像,成本文字和下订单的按钮。
我们将在一分钟内完成按钮的操作,但首先让我们完成基本布局——用以下代码替换CheckoutView
的现有body
:
GeometryReader { geo in
ScrollView {
VStack {
Image("cupcakes")
.resizable()
.scaledToFit()
.frame(width: geo.size.width)
Text("Your total is $\(self.order.cost, specifier: "%.2f")")
.font(.title)
Button("Place Order") {
// place the order
}
.padding()
}
}
}
.navigationBarTitle("Check out", displayMode: .inline)
到目前为止,这对您来说都是个旧东西。但是棘手的部分是下一个……
译自
Cupcake Corner: Introduction
Taking basic order details
Checking for a valid address
Preparing for checkout
SwiftUI:验证和禁用表单 | Hacking with iOS: SwiftUI Edition | 纸杯蛋糕项目(二) |
---|
赏我一个赞吧~~~