Core Data 与 json 转换

进行网络请求时还是 json 格式使用最多, swift 4 中新添加的Codable协议使得自定义格式与 json 转换更加方便, 不过因为 Core Data 中的NSManagedObject类的一些小问题, 不能直接遵守Codable协议, 所以分享一下自己的方法.


目录

要使用的NSManagedObject

就拿我正在写的课程表的数据模型来举例吧, 定义了下面两个NSManagedObject类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
final class CourseData: NSManagedObject {
    @NSMananaged var name: String
    @NSManaged var teacher: String
    @NSManaged var time: Set<TimeData>?
}

final class TimeData: NSManagedObject {
    @NSManaged var place: String
    @NSManaged var startseciton: Int64
    @NSManaged var endsetion: Int64
    @NSManaged var week: Int64
    @NSManaged var teachweek: [Int64]
    @NSManaged var course: CourseData
}

就是常见的课程表数据类型, 课程与上课时间建立了一对多关系. 分成两个类一方面是因为按照上课时间排课程表, 或者显示课程详情; 另一方面也方便通过关系找到对应上课时间的课程信息. 课程类中上课时间属性是可选值, 因为有的课程可能没有上课时间.

json 格式

因为网络 api 也是我写的, 所以格式和上面基本差不多:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
    "name": "操作系统",
    "teacher": "teacher"
    "time": [
    {
        "place": "综一 156",
        "startsection": 3,
        "endsection": 4,
        "week": 2,
        "teachweek": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
    },
    {
        "place": "综一 156",
        "startsection": 5,
        "endsection": 6,
        "week": 5,
        "teachweek": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
    }]
}

课程名与上课时间写在了一起, 并且上课时间是一个数组, 不是上面类中定义的集合.

自定义需要与 json 互相转换的键值

可以看到CourseDataTimeData中都有代表关系的属性, 在与 json 互相转换的过程中不需要包含这两个属性. 而且因为@NSManaged的关系, 直接遵守Codable属性后也不会转换被它标记的属性, 所以要手动定义哪些属性需要被转换.

Codable协议中默认实现了CodingKey协议的枚举, 为所有的储存属性添加了键值, 因为上面提到的原因, 现在我们要手动定义这个枚举:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// CourseData
enum CodingKeys: String, CodingKey {
    case name
    case teacher
    case time
}

// TimeData
enum CodingKeys: String, CodingKey {
    case place
    case startsection
    case endsection
    case week
    case teachweek
}

需要注意的是目前Codable协议无法应用到类的 extension 中, 所以只能在类定义内添加协议要求的方法. 在文章的最后我会放上完整的类定义代码.

定义编码

Codable协议由EncodableDecodable两个协议组成, 分别对应着编码和解码, 我们先来看编码. 编码要求我们实现encode(to encoder: Encoder) throws方法. 因为CourseData中包含TimeData对象, 所以我们先实现TimeData的编码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
// TimeData
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try! container.encode(place, forKey: .place)
    try! container.encode(startsection, forKey: .startsection)
    try! container.encode(endsection, forKey: .endsection)
    try! container.encode(week, forKey: .week)
    try! container.encode(teachweek, forKey: .teachweek)
}

首先我们以刚才定义的键值从encoder中取得一个container, container中应该包含键值和其对应的值, 接着我们调用containerencode方法为每个键赋值, 因为TimeData中每个要编码的属性都是可编码的, 所以直接赋值即可. 为了举例方便, 我没有对可能出现的异常进行处理.

对于CourseData因为刚定义好TimeData的编码方法, 所以CourseData中的属性也都可以直接进行编码了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// CourseData
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try! container.encode(name, forKey: .name)
    try! container.encode(teacher, forKey: .teacher)
    if let time = time {
        let timeArray = Array(time)
        try! container.encode(timeArray, forKey: .time)
    }
}

如果可选值为 nil, 根据 json 的语法, 对应的键也应该没有. 在 Core Data 中关系的储存方式为集合类型, 在转换成 json 的时候要记得转换为数组.

现在我们可以直接使将这两个类转换为 json:

 1
 2
 3
let encoder = JSONEncoder()
let jsonData = try! encoder.encode(courseData)
let json = String(data: jsonData, encoding: .utf8)!

定义解码

解码协议Decodable要求实现指定初始化器init(from decoder: Decoder) throws, 但是NSManagedObject有两个指定初始化器, 要想再定义其他的初始化器必须调用这两个其中的一个, 所以需要使用一些技巧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// TimeData
static var context: NSManagedObjectContext!

required convenience init(from decoder: Decoder) throws {
    self.init(entity: "TimeData", insertInto: TimeData.context)
    let container = try! decoder.container(keyedBy: CodingKeys.self)
    place = try! container.decode(String.self, forKey: .place)
    startsection = try! container.decode(Int64.self, forKey: .startsection)
    endsection = try! container.decode(Int64.self, forKey: .endsection)
    week = try! container.decode(Int64.self, forKey: .week)
    teachweek = try! container.decode([Int64].self, forKey: .teachweek)
}

为了调用指定初始化器, 我们定义一一个便利初始化器, 为了传入寝室初始化器所需要的参数, 定义了一个类变量. 不过这样做会导致使用这个初始化器之前必须先修改类变量, 为了实现从 json 初始化, 也只能这么做了. 接着还是取出一个container不过这次是从里面解码出数据.

对于CourseData, 注意取出的TimeData不能直接添加到集合中, 集合是为了表示 Core Data 中的一对多关系, 由 Core Data 维护, 直接修改会导致 Core Data 错误, 正确的做法是把CourseData添加到每一个TimeData的对单关系中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// CourseData
static var Context: NSManagedObjectContext!

required convenience init(from decoder: Decoder) throws {
    self.init(entity: "CourseData", insertInto: CourseData.context)
    let container = try! decoder.container(keyedBy: CodingKeys.self)
    name = try! container.decode(String.self, forKey: .name)
    teacher = try! container.decode(String.self, forKey: .teacher)
    TimeData.context = CourseData.context
    if let timeArray = try? container.decode([TimeData].self, forKey: .time) {
        _ = timeArray.map { $0.course = self }
    }
}

更像 Core Data 地调用

这样我们就完成对Codable协议的实现, 不过还需要再封装一下解码方法, 因为在插入一个NSManageObject的时候要指定NSManageObjectContext, 直接调用init(from decoder: Decoder)方法可能导致忘记先给context赋值而发生程序错误. 所以我们再定义一个ManagedObject协议, 规定一下和 json 转换的接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protocol ManagedObject where Self: NSManagedObject {
    static var entityName: String { get }

    static var viewContest: NSManagedObjectContext { get set }

    static func insertNewObject(from json: String, into context: NSManagedObjectContext) -> Self
    
    func exportJson() -> String
}

extension ManagedObject where Self: Codable {
    static func insertNewObject(from json: String, into context: NSManagedObjectContext) -> Self {
        let decoder = JSONDecoder()
        let jsonData = json.data(using: .utf8)!
        Self.viewContext = context
        let newObject = try! decoder.decode(Self.self, from: jsonData)
        return newObject
    }

    func exportJson() -> JSON {
        let encoder = JSONEncoder()
        let jsonData = try! encoder.encode(self)
        let json = String(data: jsonData, encoding: .utf8)!
        return json
    }
}

定义一下entityName是为了避免硬编码字符串, 不方便修改程序. 导出 json 的方法也没什么需要解释的. 从 json 新建一个NSManagedObject需要调用insertNewObject(from json: String, into context: NSManagedObjectContext)方法, 传入一个 json 的要添加到的context. 因为没什么需要在类中自定义的, 就使用协议扩展实现了默认实现.

提一下viewContext属性, 因为Codable只能在类定义中实现, 所以类定义中必须包含一个context属性. 在协议中不能调用协议中未定义的属性, 所以只好额外定义一个viewContext属性. 如果以后Codable可以应用在 extension 中, 那就可以直接让ManagedObject协议遵守Codable, 也不用这么折腾了.

两个类对协议的实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// TimeData
static var entityName: String {
    return "TimeData"
}

static var viewContext: NSManagedObjectContext {
    get { return context }
    set { context = newValue }
}

// CourseData
static var entityName: String {
    return "CourseData"
}

static var viewContext: NSManagedObjectContext {
    get { return context }
    set { context = newValue }
}

最后放一下两个类的完整定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
final class CourseData: NSManagedObject, Codable {
    fileprivate static var context: NSManagedObjectContext!
    
    @NSManaged var name: String
    @NSManaged var teacher: String
    @NSManaged var time: Set<TimeData>?
    
    enum CodingKeys: String, CodingKey {
        case name
        case teacher
        case time
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try! container.encode(name, forKey: .name)
        try! container.encode(teacher, forKey: .teacher)
        if let time = time {
            let timeArray = Array(time)
            try! container.encode(timeArray, forKey: .time)
        }
    }
    
    required convenience init(from decoder: Decoder) throws {
        self.init(entity: CourseData.entity(), insertInto: CourseData.context)
        let container = try! decoder.container(keyedBy: CodingKeys.self)
        name = try! container.decode(String.self, forKey: .name)
        teacher = try! container.decode(String.self, forKey: .teacher)
        TimeData.context = CourseData.context
        if let timeArray = try? container.decode([TimeData].self, forKey: .time) {
            _ = timeArray.map { $0.course = self }
        }
    }
}

extension CourseData: ManagedObject {
    static var viewContext: NSManagedObjectContext {
        get { return context }
        set { context = newValue }
    }
    
    static var entityName: String {
        return "CourseData"
    }
}

final class TimeData: NSManagedObject, Codable {
    fileprivate static var context: NSManagedObjectContext!
    
    @NSManaged var place: String
    @NSManaged var startsection: Int64
    @NSManaged var endsection: Int64
    @NSManaged var week: Int64
    @NSManaged var teachweek: [Int64]
    @NSManaged var course: CourseData
    
    enum CodingKeys: String, CodingKey {
        case place
        case startsection
        case endsection
        case week
        case teachweek
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try! container.encode(place, forKey: .place)
        try! container.encode(startsection, forKey: .startsection)
        try! container.encode(endsection, forKey: .endsection)
        try! container.encode(week, forKey: .week)
        try! container.encode(teachweek, forKey: .teachweek)
    }
    
    required convenience init(from decoder: Decoder) throws {
        self.init(entity: TimeData.entity(), insertInto: TimeData.context)
        let container = try! decoder.container(keyedBy: CodingKeys.self)
        place = try! container.decode(String.self, forKey: .place)
        startsection = try! container.decode(Int64.self, forKey: .startsection)
        endsection = try! container.decode(Int64.self, forKey: .endsection)
        week = try! container.decode(Int64.self, forKey: .week)
        teachweek = try! container.decode([Int64].self, forKey: .teachweek)
    }
}

extension TimeData: ManagedObject {
    static var viewContext: NSManagedObjectContext {
        get { return context }
        set { context = newValue }
    }
    
    static var entityName: String {
        return "TimeData"
    }
}

emmmm, 上次更新服务器系统时忘了备份数据库, 再加上评论比较少, 暂时关闭评论功能