iosdev

Saving native Swift struct into Core Data's transformable attribute

If your Swift struct supports Codable, you can save and restore it through Core Data persistance layer.

When designing Core Data object graph, I spend copious amount of time thinking through each entity’s attributes and relationships. If I’m not careful, I can easily end-up with expansive graph containing many one-of leaf nodes. Analysing various graphs in the past, I noticed that many complex entities (even with dozen or so attributes) are really just a complex property of their 1-1 relationship pair.

Say you have some Event object that has an elaborate set of configuration options. That configuration object is always used and accessed as a whole with the particular Event instance. Thus it should really be an attribute and not a relationship which is much harder to manage.

Let’s see how we can use Transformable attribute to save and later restore an entire struct.

First, this struct — called MarketGroupsConfiguration in this example — must support Codable to serialize its instances into Data.

Model setup

This is the usual setup of the said attribute in the Core Data modeller from Xcode:

which translates to this declaration in the model class:

@NSManaged var marketGroupsConfiguration: MarketGroupsConfigurationBox?

Wrapper class

The custom class with *Box suffix is needed because we can’t directly save Swift’s struct — it must be NSObject subclass:

final class MarketGroupsConfigurationBox: NSObject {
	let unbox: MarketGroupsConfiguration
	init(_ value: MarketGroupsConfiguration) {
		self.unbox = value
	}
}

extension MarketGroupsConfiguration {
	var boxed: MarketGroupsConfigurationBox {
	  return MarketGroupsConfigurationBox(self)
  }
}

Our wrapper class must implement NSSecureCoding which is required for recent Core Data versions:

extension MarketGroupsConfigurationBox: NSSecureCoding {
	static var supportsSecureCoding: Bool {
		return true
	}

	func encode(with coder: NSCoder) {
		do {
		  let encoder = JSONEncoder()
			let data = try encoder.encode(unbox)
			coder.encode(data, forKey: "unbox")

		} catch let codableError {
			print(codableError)
		}
	}

	convenience init?(coder: NSCoder) {
		if let data = coder.decodeObject(of: NSData.self, forKey: "unbox") {
			do {
		    let decoder = JSONDecoder()
				let b = try decoder.decode(
				  MarketGroupsConfiguration.self, 
				  from: (data as Data)
			  )
				self.init(b)

			} catch let codableError {
				print(codableError)
				return nil
			}
		} else {
			return nil
		}
	}
}

Transformer

ValueTransformer implementation is pretty involved but official documentation has enough details to get us started.

@objc(MarketGroupsConfigurationTransformer)
final class MarketGroupsConfigurationTransformer: NSSecureUnarchiveFromDataTransformer {

	static let name = NSValueTransformerName(
	  rawValue: String(describing: MarketGroupsConfigurationTransformer.self)
  )

	public static func register() {
		let transformer = MarketGroupsConfigurationTransformer()
		ValueTransformer.setValueTransformer(transformer, forName: name)
	}

	override static var allowedTopLevelClasses: [AnyClass] {
		return [
		  MarketGroupsConfigurationBox.self, 
		  NSData.self
	  ]
	}

	override public class func transformedValueClass() -> AnyClass {
		return MarketGroupsConfigurationBox.self
	}

	override public class func allowsReverseTransformation() -> Bool {
		return true
	}

	override public func transformedValue(_ value: Any?) -> Any? {
		guard let data = value as? Data else {
			return nil
		}

		do {
			let box = try NSKeyedUnarchiver.unarchivedObject(
			  ofClass: MarketGroupsConfigurationBox.self, 
			  from: data
		  )
			return box
		} catch {
			return nil
		}
	}

	override public func reverseTransformedValue(_ value: Any?) -> Any? {
		guard let box = value as? MarketGroupsConfigurationBox else {
			return nil
		}

		do {
			let data = try NSKeyedArchiver.archivedData(
			  withRootObject: box, 
			  requiringSecureCoding: true
		  )
			return data
		} catch {
			return nil
		}
	}
}

Most of these methods are metadata required for ValueTransformer to work. The essence is inside transformedValue() and reverseTransformedValue () methods which deserialise and serialise our instance.

Lastly, somewhere very early in the app lifecycle, you must register this ValueTransformer type so it can be properly recognised by iOS SDK:

MarketGroupsConfigurationTransformer.register()

I usually do this right before Core Data container / model is initialised.

And that’s it!
As you can see, this code is easily adaptable to as many transformable attributes as you need.