Custom @Model property getter/setter in SwiftData

250 views Asked by At

In Core Data I can implement a custom NSManagedObject property getter/setter:

@objc var name: String {
    get {
        willAccessValue(forKey: #keyPath(name))
        defer { didAccessValue(forKey: #keyPath(name)) }
        
        let name = primitiveValue(forKey: #keyPath(name)) as? String
        return name ?? ""
    }
    set {
        willChangeValue(forKey: #keyPath(name))
        defer { didChangeValue(forKey: #keyPath(name)) }
        
        // Trimming is just an example, it could be any data cleanup.
        let name = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
        setPrimitiveValue(name, forKey: #keyPath(name))
    }
}

How to achieve that in a SwiftData @Model? I don't want to create a second property based on a persisted one but have only a single property.

Why not 2 properties:

  • If I add the computed property later on then I need to migrate the data to a new stored property name like _name or choose a new, second-rate computed property name like nameNew.
  • The stored property cannot be private because the computed property is not available in #Predicate.
  • 1 property would be nicer because I don't really need 2 properties, I just want to clean the data in the setter.
1

There are 1 answers

0
John On

There doesn't seem to be an easy solution using a single property. But it's possible to address the pain points.

Solution 1

@Attribute(originalName: "name")
private(set) var _name = ""

var name: String {
    get { _name }
    set { _name = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
  • @Attribute(originalName: "name") is only needed if you change the stored property name from name to _name and have data to migrate. If you forget this, the data will be deleted. ⚠️
  • _name is private(set) to ensure clean data via the name setter. It cannot be fully private because as a computed property name is not available in #Predicate.

Solution 2

private(set) var name = ""

var name2: String {
    get { name }
    set { name = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
  • The stored property retains its name, so no data migration is needed when you add the computed property later.
  • The "2" in name2 stands for two-way (get and set).