JSONCache
JSONCache 是 Core Data 的薄层,可以无缝消费、缓存和产生 JSON 数据。
- 自动从 JSON 数据创建 Core Data 对象,或将 JSON 数据合并入已有的对象。
- 根据对您的 Core Data 模型的推断知识,自动映射 1:1 和 1:N 关系。
- 如有必要,自动将 JSON key 名称中的
snake_case
和 Core Data 属性名称中的camelCase
之间进行映射。 - 按需生成 JSON,既可以来自
NSManagedObject
实例,也可以来自遵循JSONifiable
协议的任何struct
。 - 在后台线程上操作,以避免干扰应用程序的响应。
阅读 API 文档 获取完整信息。
内容
显示,不要告诉
消费 JSON
假设你的后端生产以下这样的 JSON
{
"bands": [
{
"name": "Japan",
"formed": 1974,
"disbanded": 1991,
"hiatus": "1982-1989",
"description": "Initially a glam-inspired group [...]"
},
...
],
"band_members": [
{
"id": "David Sylvian in Japan",
"musician": "David Sylvian",
"band": "Japan",
"instruments": "Lead vocals, keyboards, guitar",
"joined": 1974,
"left": 1991
},
...
],
"musicians": [
{
"name": "David Sylvian",
"born": 1958,
"instruments": "Vocals, guitar, keyboards"
},
...
],
"albums": [
{
"name": "Gentlemen Take Polaroids",
"band": "Japan",
"released": "1980-10-24T00:00:00Z",
"label": "Virgin"
},
...
]
}
为了在应用中缓存这些数据,你创建一个合适的 Core Data 模型
仅通过几行代码,JSON 数据就安全地保存在设备上的 Core Data 中,包含所有关系
import JSONCache
...
let jsonObject = try! JSONSerialization.jsonObject(with: jsonData) as! [String: Any]
let bands = jsonObject["bands"] as! [[String: Any]]
let bandMembers = jsonObject["band_members"] as! [[String: Any]]
let musicians = jsonObject["musicians"] as! [[String: Any]]
let albums = jsonObject["albums"] as! [[String: Any]]
JSONCache.casing = .snake_case
JSONCache.dateFormat = .iso8601WithSeparators
JSONCache.bootstrap(withModelName: "Bands") { result in
switch result {
case .success:
JSONCache.stageChanges(withDictionaries: bands, forEntityWithName: "Band")
JSONCache.stageChanges(withDictionaries: bandMembers, forEntityWithName: "BandMember")
JSONCache.stageChanges(withDictionaries: musicians, forEntityWithName: "Musician")
JSONCache.stageChanges(withDictionaries: albums, forEntityWithName: "Album")
JSONCache.applyChanges { result in
switch result {
case .success:
print("Data all nicely tucked in")
case .failure(let error):
print("An error occurred: \(error)")
}
}
case .failure(let error):
print("An error occurred: \(error)")
}
}
如果在以后阶段收到额外的数据,操作会更简单
let albums = jsonObject["albums"] as! [[String: Any]]
JSONCache.stageChanges(withDictionaries: albums, forEntityWithName: "Album")
JSONCache.applyChanges { result in
switch result {
case .success:
print("Data all nicely tucked in")
case .failure(let error):
print("An error occurred: \(error)")
}
}
生成 JSON
如果你的应用既允许生成数据也允许消费数据,你可以直接从 NSManagedObject
实例生成 JSON
switch JSONCache.fetchObject(ofType: "Band", withId: "Japan") {
case .success(let japan):
var japan = japan as! Band
japan.otherNames = "Rain Tree Crow"
ServerProxy.update(band: japan.toJSONDictionary()) { result in
switch result {
case .success:
switch JSONCache.save() {
case .success:
print("Japan as Rain Tree Crow all nicely tucked in")
case .failure(let error):
print("An error occurred: \(error)")
}
case .failure(let error):
print("An error occurred: \(error)")
}
}
case .failure(let error):
print("An error occurred: \(error)")
}
为了创建和持久化新对象到后端,你可以首先创建 NSManagedObject
实例,然后使用它为后端生成 JSON,或者如果你更愿意等到记录安全地保存在后端,你可以从任何采用 JSONifiable
协议的 struct
生成 JSON
struct BandInfo: JSONifiable {
var name: String
var bandDescription: String
var formed: Int
var disbanded: Int?
var hiatus: Int?
var otherNames: String?
}
let u2Info = BandInfo(name: "U2", bandDescription: "Dublin boys", formed: 1976, disbanded: nil, hiatus: nil, otherNames: "Passengers")
ServerProxy.save(band: u2Info.toJSONDictionary()) { result in
switch result {
case .success:
u2 = NSEntityDescription.insertNewObject(forEntityName: "Band" into: JSONCache.mainContext)!
u2.setAttributes(fromDictionary: u2Info)
switch JSONCache.save() { result in
case .success:
print("U2 all nicely tucked in")
case .failure(let error)
print("An error occurred: \(error)")
}
case .failure(let error):
print("An error occurred: \(error)")
}
注意:我在出现
Codable
之前创建了JSONifiable
协议,文档反映了这一点。我尚未尝试使用 JSONCache 与Codable
一起使用,但据我了解,这应该会很顺利。然而,你可能需要编写一些额外的代码来获取 JSONCache 提供的免费的fromJSONDictionary
/toJSONDictionary
的一致性。话虽如此,如果我发现这样做更有意义,我将来会考虑将JSONifiable
替换为Codable
。
避免“灾难金字塔”
如上述示例所示,后端调用和许多Core Data操作的异步特性可能会导致相当严重的“死亡金字塔”(死亡金字塔)。为了避免这种情况,提供了重载的 bootstrap()
和 applyChanges()
实现方法,这些方法返回一个 ResultPromise
而不是接受一个闭包,从而方便地顺序执行计算,这些计算会产生一个 Result
(then
)或一个 ResultPromise
(thenAsync
)。
let promise = JSONCache.bootstrap(withModelName: "Bands")
.then { JSONCache.fetchObject(ofType: "Band", withId: "Japan") }
.thenAsync { object in
let japan = object as! Band
japan.otherNames = "Rain Tree Crow"
return ServerProxy.update(band: japan.toJSONDictionary())
}
.then { JSONCache.save() }
promise.await { result in
switch result {
case .success:
print("Japan as Rain Tree Crow all nicely tucked in")
case .failure(let error):
print("An error occurred: \(error)")
}
}
但请说一说
键转换
当处理JSON时,JSON键转换为Core Data实体属性名。相反,在生成JSON时,Core Data实体属性名(或struct
属性)转换为JSON键。
键转换包括两个步骤
- 根据需要将
snake_case
和camelCase
进行转换。 - 根据需要对保留字进行限定或取消限定。
大小写转换
JSONCache.casing
配置参数告诉JSONCache JSON数据中的键名是否使用.snake_case
或.camelCase
。仅当JSON使用的是.snake_case
时,才会进行大小写转换。
- 在处理JSON时,
attribute_name
转换为attributeName
。 - 在生成JSON时,
attributeName
转换为attribute_name
。
限定保留字
一些单词,如description
,与保留的Cocoa名称冲突,不能用作Core Data实体的属性名。当保留字作为JSON数据中的键接收时,它们会被限定。具体来说,是预先在保留字前面添加将存储数据的Core Data实体的名称。假设实体名称为EntityName
- 在处理JSON时,
description
变成entityNameDescription
。 - 在生成JSON时,
entityNameDescription
变成description
。
JSONCache 只支持用对应实体的名称作为前缀来限定(前缀化)保留字。因此,在命名 Core Data 实体属性时,必须小心处理其 JSON 对应词是保留字的情况。
目前,JSONCache 仅支持限定保留字 description
。未来可能还会添加更多单词。
日期转换
JSONCache 支持以下 JSON 日期格式
- 带有分隔符的 ISO 8601 格式:
2000-08-22T12:28:00Z
- 无分隔符的 ISO 8601 格式:
20000822T122800Z
- 1970 年 1 月 1 日 00:00:00 之后秒数,作为一个双精度值:
966947280.0
使用 JSONCache.dateFormat
配置参数来告知 JSONCache 预期和/或生成的格式。
关系映射
如何...
为了让 JSONCache 自动映射 1:1 或 1:N 的关系,您实际上只需告诉它一件事:关系 '1:端' 的对象的主键。您可以通过以下两种方式之一执行此操作
- 使用
id
名称作为主键。(见图 1。) - 为标识符属性创建一个名为
JC.isIdentifier
的用户信息键,并将其值设置为true
或YES
(或TRUE
或yes
;两种都不区分大小写)。(见图 2。)
图 1:通过命名 id
标记实体的主键。
图 2:通过创建一个名为 JC.isIdentifier
的用户信息键并将其设置为 true
来标记实体的主键。
主键必须在实体内部是唯一的,但不是跨实体。
但是如何...
当 JSONCache 从 JSON 字典实例化或更新 NSManagedObject
实例时,它会通过检查描述底层实体的 NSEntityDescription
的 NSAttributeDescription
来执行,并为每个属性分配来自 JSON 字典的相应值。
在第二次遍历中,JSONCache检查实体的NSRelationshipDescription
,对于每个不是toMany
的关系,它会查找目标对象的NSEntityDescription
。通过一个基于NSEntityDescription
的JSONCache扩展方法,它获取目标实体的主键。它已经从JSON字典中获取到目标对象的主键value,所以现在它拥有了查找所需的所有信息,无论是在从JSON数据中创建的新对象集合中,还是在如果已经持久化的Core Data中。一旦有目标对象的引用,它就会建立该关系并继续处理下一个项目。
考虑下面的JSON记录
乐队
{
"name": "Japan",
"formed": 1974,
"disbanded": 1991,
"hiatus": "1982-1989",
"description": "Initially a glam-inspired group [...]",
"other_names": "Rain Tree Crow"
}
专辑
{
"name": "Tin Drum",
"band": "Japan",
"released": "1981-11-13T00:00:00Z",
"label": "Virgin"
}
首先,JSONCache创建了两个NSManagedObject
实例,一个针对日本乐队,另一个针对《铁皮鼓》专辑,每个实例都包含了对应JSON记录中的所有属性。然后在第二次遍历中,JSONCache查看关系。《乐队》实体参与了两个关系,albums
和members
(参见上面的ER图),这两个都是toMany
,所以它不进行任何操作。《专辑》实体参与了一个关系,名为band
,它不是toMany
。通过描述band
关系的NSRelationshipDescription
,JSONCache发现目标实体类型为Band
,并通过描述目标实体的NSEntityDescription
,它发现它有一个主键name
。现在JSONCache拥有了所需的所有信息。它查找一个Band
对象,其name
值与《铁皮鼓》专辑在Album
字典中对应的band
值相同(即'日本'),并最终建立关系。
安装
您可以使用CocoaPods或Carthage来安装JSONCache。
CocoaPods
pod 'JSONCache'
Carthage
github "andersblehr/JSONCache" ~> 1.0
兼容性
- Swift 5.x
- macOS 10.12或更高版本
- iOS 10.0或更高版本
- watchOS 3.0或更高版本
- tvOS 10.0或更高版本
许可证
JSONCache 在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE 文件。