Data-swift/ManagedModels

Add support for transformable property types

helje5 opened this issue · 4 comments

Some infrastructure for this is in place, primarily needs to be tested and open ends need to be fixed.
Should be pretty easy.

I have the following working in production apps for years, hope it helps.
I use this every time I need to save some custom Swift struct into CoreData's Transformable attribute.

This is pretty much the contents from my upcoming blog post at aplus.rs :)

This is from CoreData modeler:
transformable-modeler

In model class declaration, this is simple:

@NSManaged internal var marketGroupsConfiguration: MarketGroupsConfigurationBox?

the *Box suffix is because I wrap Swift's struct into NSObject subclass:

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

Struct MarketGroupsConfiguration must support Codable. We now need to implement NSSecureCoding over the *Box class since it's required for recent Core Data:

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

	func encode(with coder: NSCoder) {
		do {
			let data = try unbox.encoded()
			coder.encode(data, forKey: "unbox")
		} catch let codableError {
			log(level: .warning, flowIdentifier: "", codableError)
		}
	}

	convenience init?(coder: NSCoder) {
		if let data = coder.decodeObject(of: NSData.self, forKey: "unbox") {
			do {
				let b: MarketGroupsConfiguration = try (data as Data).decoded()
				self.init(b)
			} catch let codableError {
				print(codableError)
				return nil
			}
		} else {
			return nil
		}
	}
}

What remains is now to implement the *Transformator:

//	Now, we need ability to save the `MarketGroupsConfiguration` struct into CoreData.
//	For that, we will have `Transformable` attribute which needs to map into a `class`
//	Thus the need for `MarketGroupsConfigurationBox`, which has to implement `NSSecureCoding`

@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 {
//			assertionFailure("Failed to transform `Data` to `MarketGroupsConfigurationBox`")
			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 {
//			assertionFailure("Failed to transform `MarketGroupsConfigurationBox` to `Data`")
			return nil
		}
	}
}

Lastly, somewhere very early in the app lifecycle, you must register the Transformer so it can be used:

MarketGroupsConfigurationTransformer.register()

Uh, this might actually work for other things as well 🤔 Very nice, thanks!

I've implemented Codable support (untested) in 0.6.0, but didn't try it yet.

This issue is about Attribute(.transformed(by:)) though. I've also added a lot of infra to support this, but it needs to be finished.

Looks like we don't even need the Codable box! Merged in Adam's transformable thing and also fully removed the CodableBox.

There are two things which this doesn't setup, but which doesn't seem to bother CoreData:

  • the Attribute.attributeValueClassName will stick to Any.self
  • the Transformer. transformedValueClass won't be set to anything
    Both because the precise type of the box used by Swift isn't known. I wonder whether we could? (at least for objects we could use NSStringFromClass though?)

If someone notices issues with that, please let me know!