I’m trying to get SwiftData to store Core Graphics base structures like CGSize, CGRect, CGPoint, but it doesn’t work. I suppose CGVector and CGAffineTransform could be added to the list, but I haven’t tried.
My question: What is a smart way to make SwiftData store/retrieve CG-structs?
This is what I have found:
All the CG-structs conform to Codable through conformance to Encodable and Decodable. However, they all use unkeyed encoding. Unkeyed encoding will get the simple class below to cause a crash with Fatal error: Composite Coder only supports Keyed Container
.
@Model
class TestCGSize {
// Fatal error: Composite Coder only supports Keyed Container
var cgSize: CGSize = CGSize.zero
init(cgSize: CGSize) { self.cgSize = cgSize }
}
Side note: The fatal error message is not entirely true as CGFloat works and CGFloat uses unkeyed encoding.
How can the problem be solved?
Write an extension for Codable: Can’t override an existing implementation of Encodable/Decodable with a new extension.
Write a subclass: Can’t subclass a struct.
Write a @propertyWrapper: Will generate a compiler error: Property wrapper cannot be applied to a computed property
. It seems SwiftData makes all properties into computed properties.
Use didSet observer: It actually works but only one-way. SwiftData will create the columns in the database and store the values, but I can only get retrieval to work partially. See test code below.
Write a wrapper struct: Starting on my first attempt. Any working example will be highly appreciated.
There is probably another approach I haven’t thought of. I will be grateful for any help.
@Model
class TestCGSize {
@Transient
var cgSize: CGSize = CGSize.zero {
didSet {
cgSizeWidth = cgSize.width
cgSizeHeight = cgSize.height
}
}
// Columns created and data stored in db.
// Data retrieved, but cgSize not updated
var cgSizeWidth: Double = 0 {
didSet { cgSize.width = cgSizeWidth }
}
var cgSizeHeight: Double = 0 {
willSet { cgSize.height = newValue }
}
init(cgSizeWidth: Double, cgSizeHeight: Double) {
self.cgSizeWidth = cgSizeWidth
self.cgSizeHeight = cgSizeHeight
self.cgSize.width = cgSizeWidth
self.cgSize.height = cgSizeHeight
}
}
2
Answers
@Transient
creates a second source of truth. You can keep a single source of truth by turningcgSize
into a computed property with aget
andset
.Then for continuity sake you can make the database properties
private
.This is exactly what NSValue is for. It wraps a CG-type in something that is secure codable.
https://developer.apple.com/documentation/foundation/nsvalue
The CoreData way would be to put a value transformer between the value and the database, and I presume SwiftData would therefore allow you to do the same thing.