Skip to content

Commit 7dca3bd

Browse files
committed
Codable support via transformers
Untested, should be better for performance as the primitive value doesn't encode/decode on each access.
1 parent 6ec132f commit 7dca3bd

File tree

3 files changed

+179
-18
lines changed

3 files changed

+179
-18
lines changed

Sources/ManagedModels/PersistentModel/PersistentModel+KVC.swift

Lines changed: 78 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,37 @@
55

66
import CoreData
77

8+
// Note: This needs to match up the `valueType` in NSAttributeDescription+Data.
9+
810
// MARK: - Primitives
911
public extension PersistentModel {
1012

1113
@inlinable
1214
func setValue<T>(forKey key: String, to value: T)
13-
where T: Encodable & CoreDataPrimitiveValue
15+
where T: Codable & CoreDataPrimitiveValue
1416
{
1517
willChangeValue(forKey: key); defer { didChangeValue(forKey: key) }
1618
setPrimitiveValue(value, forKey: key)
1719
}
1820

1921
@inlinable
2022
func getValue<T>(forKey key: String) -> T
21-
where T: Decodable & CoreDataPrimitiveValue
23+
where T: Codable & CoreDataPrimitiveValue
24+
{
25+
willAccessValue(forKey: key); defer { didAccessValue(forKey: key) }
26+
return primitiveValue(forKey: key) as! T
27+
}
28+
29+
@inlinable
30+
func setValue<T>(forKey key: String, to value: T)
31+
where T: Codable & CoreDataPrimitiveValue & AnyOptional
32+
{
33+
willChangeValue(forKey: key); defer { didChangeValue(forKey: key) }
34+
setPrimitiveValue(value, forKey: key)
35+
}
36+
@inlinable
37+
func getValue<T>(forKey key: String) -> T
38+
where T: Codable & CoreDataPrimitiveValue & AnyOptional
2239
{
2340
willAccessValue(forKey: key); defer { didAccessValue(forKey: key) }
2441
return primitiveValue(forKey: key) as! T
@@ -163,14 +180,14 @@ public extension PersistentModel {
163180

164181
@inlinable
165182
func setValue<T>(forKey key: String, to value: T)
166-
where T: RawRepresentable, T.RawValue: Encodable & CoreDataPrimitiveValue
183+
where T: RawRepresentable, T.RawValue: Codable & CoreDataPrimitiveValue
167184
{
168185
setValue(forKey: key, to: value.rawValue)
169186
}
170187

171188
@inlinable
172189
func getValue<T>(forKey key: String) -> T
173-
where T: RawRepresentable, T.RawValue: Decodable & CoreDataPrimitiveValue
190+
where T: RawRepresentable, T.RawValue: Codable & CoreDataPrimitiveValue
174191
{
175192
let rawValue : T.RawValue = getValue(forKey: key)
176193
guard let wrapped = T.init(rawValue: rawValue) else {
@@ -185,25 +202,68 @@ public extension PersistentModel {
185202
// SwiftData is doing a Codable a little different.
186203
// TBD: we could also use transformers for this, probably faster?!
187204

188-
@inlinable
189-
func setValue<T>(forKey key: String, to value: T) where T: Encodable {
190-
do {
191-
let data = try JSONEncoder().encode(value)
192-
setValue(forKey: key, to: data)
205+
func setValue<T>(forKey key: String, to value: T) where T: Codable {
206+
willChangeValue(forKey: key); defer { didChangeValue(forKey: key) }
207+
setPrimitiveValue(CodableBox<T>(value), forKey: key)
208+
}
209+
210+
func setValue<T>(forKey key: String, to value: T)
211+
where T: Codable & AnyOptional
212+
{
213+
willChangeValue(forKey: key); defer { didChangeValue(forKey: key) }
214+
if value.isSome { setPrimitiveValue(CodableBox<T>(value), forKey: key) }
215+
else { setPrimitiveValue(nil, forKey: key) }
216+
}
217+
218+
func getValue<T>(forKey key: String) -> T where T: Codable {
219+
willAccessValue(forKey: key); defer { didAccessValue(forKey: key) }
220+
guard let value = primitiveValue(forKey: key) else {
221+
fatalError("No box found for non-optional Codable value for key \(key)?")
222+
}
223+
224+
if let box = value as? CodableBox<T> {
225+
guard let value = box.value else {
226+
fatalError("Box has no value for non-optional Codable for key \(key)?")
227+
}
228+
return value
193229
}
194-
catch {
195-
fatalError("Could not encode JSON value for key \(key)? \(error)")
230+
231+
if let data = value as? Data {
232+
assertionFailure("Unexpected Data as primitive!")
233+
do {
234+
return try JSONDecoder().decode(T.self, from: data)
235+
}
236+
catch {
237+
fatalError("Could not decode JSON value for key \(key)? \(error)")
238+
}
196239
}
240+
241+
guard let value = value as? T else {
242+
fatalError("Unexpected value for key \(key)? \(value)")
243+
}
244+
assertionFailure("Codable value is directly stored? \(value)")
245+
return value
197246
}
198247

199-
@inlinable
200-
func getValue<T>(forKey key: String) -> T where T: Decodable {
201-
let data : Data? = getValue(forKey: key)
202-
do {
203-
return try JSONDecoder().decode(T.self, from: data ?? Data())
248+
func getValue<T>(forKey key: String) -> T where T: Codable & AnyOptional {
249+
willAccessValue(forKey: key); defer { didAccessValue(forKey: key) }
250+
guard let value = primitiveValue(forKey: key) else { return .noneValue }
251+
if let box = value as? CodableBox<T> { return box.value ?? .noneValue }
252+
253+
if let data = value as? Data {
254+
assertionFailure("Unexpected Data as primitive!")
255+
do {
256+
return try JSONDecoder().decode(T.self, from: data)
257+
}
258+
catch {
259+
fatalError("Could not decode JSON value for key \(key)? \(error)")
260+
}
204261
}
205-
catch {
206-
fatalError("Could not decode JSON value for key \(key)? \(error)")
262+
263+
guard let value = value as? T else {
264+
fatalError("Unexpected value for key \(key)? \(value)")
207265
}
266+
assertionFailure("Codable value is directly stored? \(value)")
267+
return value
208268
}
209269
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// Created by Helge Heß.
3+
// Copyright © 2023 ZeeZide GmbH.
4+
//
5+
6+
import Foundation
7+
import CoreData
8+
9+
/**
10+
* Helper object to store arbitrary Codable Swift types in a CoreData
11+
* property.
12+
*/
13+
final class CodableBox<T: Codable>: NSObject, NSCopying {
14+
// Inspired by @radianttap
15+
16+
var value : T?
17+
18+
init(_ value: T) { self.value = value }
19+
20+
private init(_ value: T?) { self.value = value }
21+
22+
private init?(data: Data) {
23+
do {
24+
value = try JSONDecoder().decode(T.self, from: data)
25+
}
26+
catch {
27+
assertionFailure("Could not decode JSON value of property? \(error)")
28+
value = nil
29+
}
30+
}
31+
32+
func copy(with zone: NSZone? = nil) -> Any { CodableBox<T>(self.value) }
33+
34+
var data : Data? {
35+
set {
36+
guard let data = newValue else {
37+
value = nil
38+
return
39+
}
40+
do {
41+
value = try JSONDecoder().decode(T.self, from: data)
42+
}
43+
catch {
44+
assertionFailure("Could not decode JSON value of property? \(error)")
45+
value = nil
46+
}
47+
}
48+
get {
49+
guard let value else { return nil }
50+
do {
51+
return try JSONEncoder().encode(value)
52+
}
53+
catch {
54+
assertionFailure("Could not encode JSON value of property? \(error)")
55+
return nil
56+
}
57+
}
58+
}
59+
60+
final class Transformer: ValueTransformer {
61+
62+
override class func transformedValueClass() -> AnyClass {
63+
CodableBox<T>.self
64+
}
65+
override class func allowsReverseTransformation() -> Bool { true }
66+
67+
override func transformedValue(_ value: Any?) -> Any? {
68+
// value is the box
69+
guard let value else { return nil }
70+
guard let typed = value as? CodableBox<T> else {
71+
assertionFailure("Value to be transformed is not the box? \(value)")
72+
return nil
73+
}
74+
return typed.data
75+
}
76+
77+
override func reverseTransformedValue(_ value: Any?) -> Any? {
78+
guard let value else { return nil }
79+
guard let data = value as? Data else { return nil }
80+
return CodableBox<T>(data: data)
81+
}
82+
}
83+
}

Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@ extension CoreData.NSAttributeDescription: SchemaProperty {
6565
if setIt(for: rawType) { return }
6666
}
6767

68+
if let codableType = newValue as? any Codable.Type {
69+
// TBD: Someone tell me whether this is sensible.
70+
self.attributeType = .transformableAttributeType
71+
self.isOptional = newValue is any AnyOptional.Type
72+
73+
func setValueClassName<T: Codable>(for type: T.Type) {
74+
self.attributeValueClassName = NSStringFromClass(CodableBox<T>.self)
75+
76+
let name = NSStringFromClass(CodableBox<T>.Transformer.self)
77+
if !ValueTransformer.valueTransformerNames().contains(.init(name)) {
78+
// no access to valueTransformerForName?
79+
let transformer = CodableBox<T>.Transformer()
80+
ValueTransformer
81+
.setValueTransformer(transformer, forName: .init(name))
82+
}
83+
valueTransformerName = name
84+
}
85+
setValueClassName(for: codableType)
6886
}
6987

7088
// TBD:

0 commit comments

Comments
 (0)