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

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

编码一个 ObservableObject 类

我们对代码进行了组织,以使我们在所有界面之间共享一个Order对象,其优点是我们可以在这些界面之间来回移动而不会丢失数据。但是,这种方法需要付出一定的代价:我们必须对类中的属性使用@Published属性包装器,并且一旦这样做,我们就失去了自动Codable的支持。

如果您不相信我,只需尝试修改Order的定义以包含Codable,如下所示:

class Order: ObservableObject, Codable {

现在构建将失败,因为Swift不了解如何编码和解码已发布的属性。这是一个问题,因为我们想将用户的订单提交到互联网服务器,这意味着我们需要将其作为JSON :我们需要Codable协议才能正常工作。

解决方法是手动添加Codable适配,这意味着告诉Swift应该编码什么,应该如何编码以及应该如何解码——从JSON转换回Swift数据。

第一步意味着添加一个符合CodingKey的枚举,列出我们要保存的所有属性。在我们的Order类中,几乎所有内容——唯一不需要的是静态的 types属性。

因此,将此枚举添加到Order中:

enum CodingKeys: CodingKey {
    case type, quantity, extraFrosting, addSprinkles, name, streetAddress, city, zip
}

第二步要求我们编写一个encode(to:)方法,该方法使用刚刚创建的编码键枚举创建一个容器,然后写出附加到其各自键的所有属性。只需反复调用一次encode(_:forKey:),每次传入一个不同的属性和编码键即可。

现在将此方法添加到Order

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)

    try container.encode(type, forKey: .type)
    try container.encode(quantity, forKey: .quantity)

    try container.encode(extraFrosting, forKey: .extraFrosting)
    try container.encode(addSprinkles, forKey: .addSprinkles)

    try container.encode(name, forKey: .name)
    try container.encode(streetAddress, forKey: .streetAddress)
    try container.encode(city, forKey: .city)
    try container.encode(zip, forKey: .zip)
}

由于该方法带有throws标记,因此我们不必担心捕获内部抛出的任何错误——我们可以使用try而不添加catch,因为知道任何问题都会自动向上传播并在其他地方处理。

我们的最后一步是实现所需的初始化器,以从某些已归档数据中解码Order实例。这几乎与编码相反,甚至可以从相同的throws功能中受益:

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    type = try container.decode(Int.self, forKey: .type)
    quantity = try container.decode(Int.self, forKey: .quantity)

    extraFrosting = try container.decode(Bool.self, forKey: .extraFrosting)
    addSprinkles = try container.decode(Bool.self, forKey: .addSprinkles)

    name = try container.decode(String.self, forKey: .name)
    streetAddress = try container.decode(String.self, forKey: .streetAddress)
    city = try container.decode(String.self, forKey: .city)
    zip = try container.decode(String.self, forKey: .zip)
}

值得补充的是,您可以按任意顺序对数据进行编码——无需匹配对象中声明属性的顺序。

这使得我们的代码完全符合Codable:我们有效地绕过了@Published属性包装器,直接读取和写入值。但是,它并不能使我们的代码编译——实际上,我们现在在 ContentView.swift 中返回了一个完全不同的错误。

现在的问题是,我们只是为Orderinit(from :)创建了一个自定义初始化器,而Swift希望我们在任何地方都可以使用它——即使在由于应用程序刚刚启动而只想创建一个新的空订单的地方。

幸运的是,Swift使我们可以向一个类中添加多个初始化器,以便我们可以以多种不同方式创建它。在这种情况下,这意味着我们需要编写一个新的初始化程序,该初始化程序可以创建不包含任何数据的订单——它将完全依赖于我们分配的默认属性值。

因此,将此新的初始化器添加到Order中:

init() { }

现在,我们的代码又回到了编译过程,并且我们的Codable支持已完成。这意味着我们已准备好进行最后一步:通过网络发送和接收Order对象。

通过互联网发送和接收订单

iOS提供了一些极好的处理网络的功能,特别是URLSession类使得发送和接收数据变得异常容易。如果我们结合使用Codable将Swift对象转换为JSON和URLRequest(这让我们能够准确地配置数据的发送方式),我们可以在大约20行代码中完成伟大的事情。

首先,让我们创建一个可以从下单按钮调用的方法——将此添加到CheckoutView

func placeOrder() {
}

现在将“下订单”按钮修改为:

Button("Place Order") {
    self.placeOrder()
}
.padding()

placeOrder()中,我们需要做三件事:

  1. 将当前的order对象转换为一些可以发送的JSON数据。
  2. 准备一个URLRequest以JSON格式发送我们的编码数据。
  3. 运行该请求并处理响应。

第一个是直截了当的,所以让我们把它让开。我们已经使Order类符合Codable,这意味着我们可以使用JSONEncoder通过将此代码添加到placeOrder()来存档它:

guard let encoded = try? JSONEncoder().encode(order) else {
    print("Failed to encode order")
    return
}

第二步:准备URLRequest来发送我们的数据——需要更多的思考。你看,我们需要以非常特定的方式附加数据,以便服务器能够正确处理它,这意味着我们需要提供两个额外的数据片段,而不仅仅是我们的订单:

  1. 请求的HTTP方法决定如何发送数据。有几种HTTP方法,但实际上只使用GET(“我想读取数据”)和POST(“我想读取\写入数据”)。我们想在这里写数据,所以我们将使用POST。
  2. 请求的内容类型决定了要发送的数据类型,这会影响服务器处理数据的方式。这是在所谓的 MIME 类型中指定的,它最初是用来在电子邮件中发送附件的,它有数千个特定的选项。

因此,placeOrder() 的下一个代码将是创建一个URLRequest,将其配置为使用HTTP POST发送JSON数据,并附加我们的数据。

当然,真正的问题是在哪里发送我们的请求,我不认为您真的想设置您自己的web服务器来遵循本教程。因此,我们将使用一个非常有用的网站,名为https://reqres.in, (国内 Easy Mock也能提供类似服务),它让我们发送任何我们想要的数据,并将自动发回。这是制作网络代码原型的一种好方法,因为无论发送什么,都可以从中获取真实的数据。

现在将此代码添加到placeOrder()中:

let url = URL(string: "https://reqres.in/api/cupcakes")!
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = encoded

请注意,我是如何为URL(string:)初始化器添加强制解包的。从字符串创建 URL 可能会失败,因为您插入了一些乱七八糟的东西,但是在这里我手动键入了URL,因此我可以看到它总是正确的——那里没有字符串内插法可能会引起问题。

至此,我们已经准备好发出网络请求,我们将使用URLSession.shared.dataTask()和我们刚刚发出的URL请求来完成。请记住,如果您不对数据任务调用resume(),它将永远不会启动,这就是为什么我几乎总是编写任务并在实际填充正文之前调用resume的原因。

因此,将其添加到placeOrder()中:

URLSession.shared.dataTask(with: request) { data, response, error in
    // handle the result here.
}.resume()

现在进行重要工作:我们需要阅读结果以获取我们的请求。如果出现问题,也许是因为没有互联网连接,我们将只打印一条消息并返回。

将下面代码添加到placeOrder()

guard let data = data else {
    print("No data in response: \(error?.localizedDescription ?? "Unknown error").")
    return
}

如果超过此范围,则意味着我们从服务器返回了某种数据。因为我们使用的是 ReqRes.in,所以实际上我们将返回到发送的相同顺序,这意味着我们可以使用JSONDecoder将其从JSON转换回对象。

为了确认一切正常,我们将显示一个包含订单详细信息的警报,但我们将使用从 ReqRes.in 返回的已解码订单。是的,这应该与我们发送的相同,因此,如果不是,则意味着我们在编码中犯了一个错误。

显示警报需要属性来存储消息以及消息是否可见,因此请立即将这两个新属性添加到CheckoutView中:

@State private var confirmationMessage = ""
@State private var showingConfirmation = false

我们还需要附加一个alert()修饰符来监视该布尔值,并在其为true时立即显示该警报。将此修改器添加到CheckoutViewGeometryReader中:

.alert(isPresented: $showingConfirmation) {
    Alert(title: Text("Thank you!"), message: Text(confirmationMessage), dismissButton: .default(Text("OK")))
}

现在,我们可以完成网络代码:我们将解码返回的数据,使用它来设置我们的确认消息属性,然后将showingConfirmation设置为true,以便出现警报。如果解码失败——如果服务器由于某种原因发回了一些订单以外的东西——我们只会打印一条错误消息。

将此最终代码添加到placeOrder()中,就在dataTask(with:)的完成闭包中:

if let decodedOrder = try? JSONDecoder().decode(Order.self, from: data) {
    self.confirmationMessage = "Your order for \(decodedOrder.quantity)x \(Order.types[decodedOrder.type].lowercased()) cupcakes is on its way!"
    self.showingConfirmation = true
} else {
    print("Invalid response from server")
}

有了最终代码,我们的网络代码就完成了,实际上我们的应用程序也已经完成了。如果您现在尝试运行它,则应该能够选择所需的确切蛋糕,输入交货信息,然后按下订单以查看警告!

完成了!好了,我完成了——您仍然有一些挑战需要完成!

译自
Encoding an ObservableObject class
Sending and receiving orders over the internet

纸杯蛋糕项目(一) Hacking with iOS: SwiftUI Edition 纸杯蛋糕项目——挑战

赏我一个赞吧~~~

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