SwiftUI:把对象保存成JSON
涉及知识点
- 如何使用
Codable编码、解码对象; - 如何使用
JSONEncoder将Codable对象变成Data对象; - 如何将
Data对象保存到文件系统中; - 如何从
URL(文件路径或网络地址)加载Codable对象; - 四个编译器自动生成代码的
protocol。
文章内容
这篇文章以Shape对象为例子,说明怎么讲对象保存成JSON。Shape对象定义如下:
struct Shape {
var color: ShapeColor
var type: ShapeType
var isPicked: Bool
}
enum ShapeColor {
case red, green, blue, none
}
enum ShapeType {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case square(edgeLength: Double)
}
Shape对象是虚构的,并不来自某段程序,但你可以把它想成某个画板工具的数据模型。
Codable、Decodable和Encodable协议
要想让Shape变成JSON,以及想从JSON中构建Shape,我们需要先让Shape对象遵从Codable协议。该协议的定义如下:
typealias Codable = Decodable & Encodable
我们可以看看Encodable和Decodale分别是怎么定义的:
/// A type that can encode itself to an external representation.
public protocol Encodable {
/// Encodes this value into the given encoder.
///
/// If the value fails to encode anything, `encoder` will encode an empty
/// keyed container in its place.
///
/// This function throws an error if any values are invalid for the given
/// encoder's format.
///
/// - Parameter encoder: The encoder to write data to.
func encode(to encoder: Encoder) throws
}
/// A type that can decode itself from an external representation.
public protocol Decodable {
/// Creates a new instance by decoding from the given decoder.
///
/// This initializer throws an error if reading from the decoder fails, or
/// if the data read is corrupted or otherwise invalid.
///
/// - Parameter decoder: The decoder to read data from.
init(from decoder: Decoder) throws
}
只要对象中所有的属性都遵守Codable协议,那么Swift编译器就会自动帮你生成这两个函数,所以除非你有特殊要求,我们并不需要编写这两个函数。Swift中所有常见的类型,如Int、String、URL、数组、字典都遵从Codable协议。对于enum,只要所有枚举值参数都是Codable,或者压根就没有参数,那么编译器就会帮你生成。我们的ShapeColor和ShapeType都满足这样的条件,所以我们直接加上Codable协议即可:
struct Shape: Codable {
var color: ShapeColor
var type: ShapeType
var isPicked: Bool
}
enum ShapeColor: Codable {
case red, green, blue, none
}
enum ShapeType: Codable {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case square(edgeLength: Double)
}
顺便说说,在Swift中有四个特殊的协议,如果满足条件,那么Swift编译器就会自动生成默认实现,用户无需自己定义。
Encodable:自动生成func encode(to encoder: Encoder) throws;Decodable:自动生成init(from decoder: Decoder) throws;Equatable:自动生成static func ==(a: Type, b: Type),其中Type是对象类型;Hashable: 自动生成var hashValue: Int { get }。
还记得typealias Codable = Encodable & Decodable吗?使用它,就等于使用Encodable和Docodable两个协议。
你可以从这里看到更详细的规则:https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md
关于怎么使用Codable,可以阅读这篇文章:https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
控制输出JSON的内容
通过自己编写func encode(to encoder: Encoder) throws和init(from decoder: Decoder),我们就能控制输出JSON的内容和格式。比如,我们规定Shape输出成JSON时,需要满足以下几点要求:
Shape.isPicked不应该出现在JSON中;- 如果
Shape.color = none,那么color字段也不应该出现在JSON中。
那么,我可以通过自己定义encode(to encoder:)与init(from decoder:),满足上面的要求。
struct Shape: Codable {
var color: ShapeColor
var type: ShapeType
var isPicked: Bool
enum CodingKeys: String, CodingKey {
case color, type
}
init(color: ShapeColor, type: ShapeType, isPicked: Bool = false) {
self.color = color
self.type = type
self.isPicked = isPicked
}
init(from decoder: Decoder) throws {
var container = try decoder.container(keyedBy: CodingKeys.self)
let color = try container.decodeIfPresent(ShapeColor.self, forKey: .color)
self.color = color ?? .none
try self.type = container.decode(ShapeType.self, forKey: .type)
self.isPicked = false
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if case .none = color {} else {
try container.encode(color, forKey: .color)
}
try container.encode(type, forKey: .type)
}
}
将Codable对象变成JSON文件
要将Codable对象变成变成JSON文件,需要用到JSONEncoder。顾名思义,它就是用来将Encodable对象编码成JSON的。它的用法很简单:创建一个JSONEncoder对象,调用其write方法,然后就可以得到一个包含JSON数据的Data对象了。注意在使用前需要import Foundation。
import Foundation
let shape = Shape(color: .red, type: .circle(radius: 5))
let data = try JSONEncoder().encode(shape)
Data对象
我们可以将Data对象变成字符串打印出来,这样我们就得到Shape对象编码成JSON的样子。
let jsonString = String(data: data, encoding: .utf8)
pirnt(jsonString)
// in the console: {"type":{"circle":{"radius":5}}}
当然,我们也可以将Data存到文件系统中,要做到这一步,只需一步:
try data.write(theURLWhereTheFileIsStored)
获取保存的文件路径
通常我们会把文件存放到特定的目录下,比如说“documentDirectory”,在iOS下它是“文件”App的本地根目录,在macOS下是用户的“文稿”目录。下面函数可以用来获取到“documentDirectory”的URL:
func getDocumentDirectoryURL() -> URL? {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
}
然后我们可以使用URL.appending方法,把文件名filename加上去,以获得完整的保存路径:
let theURLWhereTheFileIsStored = getDocumentDirectoryURL()?.appending(path: filename)
有了完整的保存路径,我们只需要调用data.write,就成功将Shape对象存到文件系统中了。我们可以整理整理上边的代码,然后在Shape对象上添加一个公开的对象方法。调用它,就能保存Shape对象。
struct Shape: Codable {
// ...
func saveToFile(filename: String) throws {
guard let documentDirectory = getDocumentDirectoryURL() else { return }
let theURLWhereTheFileIsStored = documentDirectory.appending(path: filename)
let data = try JSONEncoder().encode(self)
try data.write(to: theURLWhereTheFileIsStored)
}
private func getDocumentDirectoryURL() -> URL? {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
}
}
从文件中读取Shape
我们可以写一个构造器,让Shape从一个URL中读取初始化数据。这样我们就能够从文件中读取并初始化Shape了。(不仅仅如此,由于输入的是URL,所以我们也能够从网络下载数据,并使用这些数据初始化Shape!)
struct Shape {
// ...
init(fromURL url: URL) throws {
let data = try Data(contentsOf: url)
let shape = try JSONDecoder().decode(Shape.self, from: data)
}
}
完整例子
import Foundation
struct Shape: Codable {
var color: ShapeColor
var type: ShapeType
var isPicked: Bool
enum CodingKeys: String, CodingKey {
case color, type
}
init(color: ShapeColor, type: ShapeType, isPicked: Bool = false) {
self.color = color
self.type = type
self.isPicked = isPicked
}
init(from decoder: Decoder) throws {
var container = try decoder.container(keyedBy: CodingKeys.self)
let color = try container.decodeIfPresent(ShapeColor.self, forKey: .color)
self.color = color ?? .none
try self.type = container.decode(ShapeType.self, forKey: .type)
self.isPicked = false
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if case .none = color {} else {
try container.encode(color, forKey: .color)
}
try container.encode(type, forKey: .type)
}
init(fromURL url: URL) throws {
let data = try Data(contentsOf: url)
let shape = try JSONDecoder().decode(Shape.self, from: data)
}
func saveToFile(filename: String) throws {
guard let documentDirectory = getDocumentDirectoryURL() else { return }
let theURLWhereTheFileIsStored = documentDirectory.appending(path: filename)
let data = try JSONEncoder().encode(self)
try data.write(to: theURLWhereTheFileIsStored)
}
private func getDocumentDirectoryURL() -> URL? {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
}
}
enum ShapeColor: Codable {
case red, green, blue, none
}
enum ShapeType: Codable {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case square(edgeLength: Double)
}