Hacking with iOS: SwiftUI Edition - 纸杯蛋糕项目(一)

Hacking with iOS: SwiftUI Edition - 纸杯蛋糕项目(一)_第1张图片
cake - 韦弦zhy

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之间的步进器来选择数量。所有这些都将包装在一个表单中,而表单本身又位于导航视图中,因此我们可以设置标题。

现在将其放入ContentViewbody中:

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更新中消失(此错误已经修复)。

我们表单的第二部分将包含三个分别绑定到specialRequestEnabledextraFrostingaddSprinkles的切换开关。不过,只有在启用第一个和第二个开关的情况下,第二个和第三个开关才应该可见,因此我们将其包装为一个条件。

现在添加第二部分:

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时将extraFrostingaddSprinkles都重置为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可以正常工作了,除非满足某些条件,否则是时候停止用户进行结帐了。什么条件?好吧,这取决于我们来决定。尽管我们可以为四个文本字段中的每一个编写长度检查,但这经常使人们感到困惑——有些名称只有四个或五个字母,因此,如果您尝试添加长度验证,则可能会意外地排除某些用户。

因此,我们只需要检查订单的namestreetAddresscityzip属性是否为空。我更喜欢在数据中添加这种复杂的检查,这意味着您需要像下面这样向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 纸杯蛋糕项目(二)

赏我一个赞吧~~~

你可能感兴趣的:(Hacking with iOS: SwiftUI Edition - 纸杯蛋糕项目(一))