JSONCache 1.3

JSONCache 1.3

测试已测试
Lang语言 SwiftSwift
许可证 MIT
发布最后发布2019年10月
SPM支持 SPM

Anders Blehr 维护。



JSONCache

Build Status Carthage compatible CocoaPods Platform license MIT

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 而不是接受一个闭包,从而方便地顺序执行计算,这些计算会产生一个 Resultthen)或一个 ResultPromisethenAsync)。

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键。

键转换包括两个步骤

  1. 根据需要将snake_casecamelCase进行转换。
  2. 根据需要对保留字进行限定或取消限定。

大小写转换

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:端' 的对象的主键。您可以通过以下两种方式之一执行此操作

  1. 使用 id 名称作为主键。(见图 1。)
  2. 为标识符属性创建一个名为 JC.isIdentifier 的用户信息键,并将其值设置为 trueYES(或 TRUEyes;两种都不区分大小写)。(见图 2。)

图 1:通过命名 id 标记实体的主键。

图 2:通过创建一个名为 JC.isIdentifier 的用户信息键并将其设置为 true 来标记实体的主键。

主键必须在实体内部是唯一的,但不是跨实体。

但是如何...

当 JSONCache 从 JSON 字典实例化或更新 NSManagedObject 实例时,它会通过检查描述底层实体的 NSEntityDescriptionNSAttributeDescription 来执行,并为每个属性分配来自 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查看关系。《乐队》实体参与了两个关系,albumsmembers(参见上面的ER图),这两个都是toMany,所以它不进行任何操作。《专辑》实体参与了一个关系,名为band,它不是toMany。通过描述band关系的NSRelationshipDescription,JSONCache发现目标实体类型为Band,并通过描述目标实体的NSEntityDescription,它发现它有一个主键name。现在JSONCache拥有了所需的所有信息。它查找一个Band对象,其name值与《铁皮鼓》专辑在Album字典中对应的band值相同(即'日本'),并最终建立关系。

安装

您可以使用CocoaPodsCarthage来安装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 文件。