Transformable types

Andreea Andro
3 min readJan 28, 2023

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.

  1. 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)
How to define the attribute

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.

How to map the transformer class

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.

--

--