Search code examples
iosswiftcodableswiftdata

Unable to get SwiftData to store/retrieve CGPoint, CGSize, CGRect


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
    }
}

Solution

  • @Transient creates a second source of truth. You can keep a single source of truth by turning cgSize into a computed property with a get and set.

    Then for continuity sake you can make the database properties private.

    import SwiftData
    @Model
    class TestCGSize {
        var cgSize: CGSize {
            get {
                .init(width: _cgSizeWidth, height: _cgSizeHeight)
            }
            set {
                self._cgSizeWidth = newValue.width
                self._cgSizeHeight = newValue.height
            }
        }
        
        // Columns created and data stored in db.
        private var _cgSizeWidth: Double = 0.0
        private var _cgSizeHeight: Double = 0.0
        
        init(cgSize: CGSize) {
            self.cgSize = cgSize
        }
    }