Skip to main content

How to create derived attributes with SwiftData

About 2 minSwiftArticle(s)bloghackingwithswift.comcrashcourseswiftswiftdataxcodeappstore

How to create derived attributes with SwiftData 관련

SwiftData by Example

Back to Home

How to create derived attributes with SwiftData | SwiftData by Example

How to create derived attributes with SwiftData

Updated for Xcode 15

Derived attributes were a power feature of Core Data, allowing us to automatically calculate or update some properties dynamically. Sadly they don’t exist in SwiftData, however we can sort of work around this with some extra effort.

Important

Derived attributes in Core Data are extremely efficient because they are implemented as triggers at the database level. Our workarounds are nothing like as efficient!

We have three options here.

The first option is the simplest: we derive the value ourselves as a computed property, which at least puts the functionality in place until we hopefully get official support for real derived attributes in the future.

For example, in Core Data it was common to use derivations like @count or @sum, so we can implement these by hand:

@Model
class School {
    var students: [Student]

    var studentCount: Int {
        students.count
    }

    init(students: [Student]) {
        self.students = students
    }
}

@Model
class Student {
    var name: String

    init(name: String) {
        self.name = name
    }
}

If that were implemented as a derived attribute, Core Data would be able to calculate the number of students directly rather than fetching the whole array then returning its count property, but like I said it’s just a workaround.

That option works well for some derived attributes, but in my experience the most common thing I want to derive is some kind of lastModified property that automatically updates whenever any value is changed.

That wouldn’t work with a simple computed property, so here there’s a second option: create a generic update() method that can adjust any value inside your model, but while doing automatically updates some other value.

For example, this code creates a Player class with a lastModified property, along with an update() method that can adjust any value while also changing lastUpdated to the current date and time:

@Model class Player {
    var name: String
    var score: Int
    var lastModified: Date

    init(name: String, score: Int) {
        self.name = name
        self.score = score
        self.lastModified = .now
    }

    func update<T>(keyPath: ReferenceWritableKeyPath<Player, T>, to value: T) {
        self[keyPath: keyPath] = value
        lastModified = .now
    }
}

With that in place, we can use user.update(keyPath: \.score, to: 10) to change the score property and also update lastModified.

Note

SwiftData properties do not support willSet or didSet property observers, so we’re effectively bouncing our changes through a new method in lieu of using didSet.

A third option is to create a new @Transient attribute for the values you want. Unlike regular SwiftData properties, @Transient properties do support willSet and didSet property observers, but honestly this is more trouble than it’s worth – you’d need transient properties for everything you want to watch, so you’re creating a huge amount of work for yourself.

Let’s hope we either get regular didSet and willSet support soon, or, better yet, actual derived attributes implemented at the database level…


이찬희 (MarkiiimarK)
Never Stop Learning.