编码一个 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 中返回了一个完全不同的错误。
现在的问题是,我们只是为Order
类init(from :)
创建了一个自定义初始化器,而Swift希望我们在任何地方都可以使用它——即使在由于应用程序刚刚启动而只想创建一个新的空订单的地方。
幸运的是,Swift使我们可以向一个类中添加多个初始化器,以便我们可以以多种不同方式创建它。在这种情况下,这意味着我们需要编写一个新的初始化程序,该初始化程序可以创建不包含任何数据的订单——它将完全依赖于我们分配的默认属性值。
因此,将此新的初始化器添加到Order
中:
init() { }
现在,我们的代码又回到了编译过程,并且我们的Codable
支持已完成。这意味着我们已准备好进行最后一步:通过网络发送和接收Order
对象。
通过互联网发送和接收订单
iOS提供了一些极好的处理网络的功能,特别是URLSession
类使得发送和接收数据变得异常容易。如果我们结合使用Codable
将Swift对象转换为JSON和URLRequest
(这让我们能够准确地配置数据的发送方式),我们可以在大约20行代码中完成伟大的事情。
首先,让我们创建一个可以从下单按钮调用的方法——将此添加到CheckoutView
:
func placeOrder() {
}
现在将“下订单”按钮修改为:
Button("Place Order") {
self.placeOrder()
}
.padding()
在placeOrder()
中,我们需要做三件事:
- 将当前的
order
对象转换为一些可以发送的JSON数据。 - 准备一个
URLRequest
以JSON格式发送我们的编码数据。 - 运行该请求并处理响应。
第一个是直截了当的,所以让我们把它让开。我们已经使Order
类符合Codable
,这意味着我们可以使用JSONEncoder通过将此代码添加到placeOrder()
来存档它:
guard let encoded = try? JSONEncoder().encode(order) else {
print("Failed to encode order")
return
}
第二步:准备URLRequest
来发送我们的数据——需要更多的思考。你看,我们需要以非常特定的方式附加数据,以便服务器能够正确处理它,这意味着我们需要提供两个额外的数据片段,而不仅仅是我们的订单:
- 请求的HTTP方法决定如何发送数据。有几种HTTP方法,但实际上只使用GET(“我想读取数据”)和POST(“我想读取\写入数据”)。我们想在这里写数据,所以我们将使用POST。
- 请求的内容类型决定了要发送的数据类型,这会影响服务器处理数据的方式。这是在所谓的 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时立即显示该警报。将此修改器添加到CheckoutView
的GeometryReader
中:
.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 | 纸杯蛋糕项目——挑战 |
---|
赏我一个赞吧~~~