Transformable types
Saving a custom type with Core Data.
Let’s say we need to persistently save the info from bellow, representing the speeds we can work with in our app.
"motor_configuration": {
"max_speed_limit": {
"20mph": [100, 101],
"25kph": [200, 201],
"28mph": [300, 301],
"25kphTW": [150],
"25kphJP": [160]
}
}
It’s a dictionary of max speed limits.
- the key is a human readable speed written as a String
- the value is a list of equivalent speeds expressed as Integers in a certain unit of measure
We don’t know how many elements will be in the dictionary or in the arrays.
How would you save this data?
I’ll show how to save it in CoreData as a transformable type.
- Create a new Entity, let’s name it CustomAppSetting, with an attribute motorConfiguration, of type Transformable. When using the Transformable type, we have to provide values for
- “Custom Class”, I’ll call it MotorConfiguration (and I’ll define it later)
- “Transformer”, I’ll call it MotorConfigurationTransformer (and I’ll define it later)
2. Define the custom classes
public class MotorConfiguration: NSObject {
let maxSpeedLimit: [String: [Int]]
init(maxSpeedLimit: [String: [Int]]) {
self.maxSpeedLimit = maxSpeedLimit
super.init()
}
}
class MotorConfigurationTransformer: ValueTransformer {
override func transformedValue(_ value: Any?) -> Any? {
guard let motorConfiguration = value as? MotorConfiguration else {
print("\(#function) cast issue")
return nil
}
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: motorConfiguration, requiringSecureCoding: true)
return data
} catch {
print(error)
return nil
}
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else {
print("\(#function) nil data value")
return nil
}
do {
let motorConfiguration = try NSKeyedUnarchiver
.unarchivedObject(ofClasses: [
MotorConfiguration.self, NSDictionary.self,
NSArray.self, NSString.self, NSNumber.self],
from: data)
return motorConfiguration
} catch {
print(error)
return nil
}
}
}
- For a value of type MotorConfiguration to be marked as `@NSManaged`, MotorConfiguration needs to be represented in Objective-C. So it should conform to NSObject.
- MotorConfiguration also needs to adopt NSSecureCoding to avoid the error “This decoder will only decode classes that adopt NSSecureCoding. Class ‘TransformableType.MotorConfiguration’ does not adopt it.”
- MotorConfigurationTransformer needs to tell how to do the encoding and decoding of the object. I’m specifying all the types used inside of the object that I store.
3. Map the transformer class we just defined to its name used previously in the Model in the “Transformer” field.
To test this, I’ll use some mock values:
extension CustomAppSetting {
static func insert(_ config: MotorConfiguration) {
let entity = CustomAppSetting(context: CoreDataManager.shared.persistentContainer.viewContext)
entity.motorConfiguration = config
try? CoreDataManager.shared.persistentContainer.viewContext.save()
print("Created CustomAppSetting with configs \(entity.motorConfiguration?.maxSpeedLimit)")
}
}
// In ContentView or some other UI
Button("Insert values in DB") {
CustomAppSetting.insert(MotorConfiguration(maxSpeedLimit: [
"20mph": [100, 101],
"25kph": [120, 121],
"28mph": [150, 151, 152],
"25kphTW": [50],
"25kphJP": [45]
]))
}
The entity was well created and persisted in the DB. You can see the project on github.