SwiftData: How to model inheritance?

647 views Asked by At

My model class includes an array of objects that have some properties in common and some specific to only a subset. That sounds like a use case for a protocol and inheritance. However, I can't get SwiftData to compile this.

Minimal example:

import Foundation
import SwiftData

@Model
class Workout {
    var startTime: Date
    var endTime: Date?
    var activities: [Activity]
    
    init(startTime: Date, endTime: Date? = nil, activities: [Activity]) {
        self.startTime = startTime
        self.endTime = endTime
        self.activities = activities
    }
}

protocol Activity {
    var calories: Int {get}
}

@Model
class Run: Activity {
    var calories: Int
    var steps: Int
    
    init(calories: Int, steps: Int) {
        self.calories = calories
        self.steps = steps
    }
}

@Model
class Swim: Activity {
    var calories: Int
    var lanes: Int
    
    init(calories: Int, lanes: Int) {
        self.calories = calories
        self.lanes = lanes
    }
}

The error Type 'any Activity' cannot conform to 'PersistentModel' occurs in the generated code for the array:

enter image description here

I watched the WWDC videos, read through the docs and looked at all example projects I could find — nowhere is this problem tackled.

Does anyone have an idea either how to make SwiftData to support this or how to model the data in a compatible way?

2

There are 2 answers

2
windowcow On
@Model
class Workout<T: Activity> {
    var startTime: Date
    var endTime: Date?
    var activities: [T]
    
    init(startTime: Date, endTime: Date? = nil, activities: [T]) {
        self.startTime = startTime
        self.endTime = endTime
        self.activities = activities
    }
}

protocol Activity: PersistentModel {
    var calories: Int {get}
}

@Model
class Run: Activity {
    var calories: Int
    var steps: Int
    
    init(calories: Int, steps: Int) {
        self.calories = calories
        self.steps = steps
    }
}

@Model
class Swim: Activity {
    var calories: Int
    var lanes: Int
    
    init(calories: Int, lanes: Int) {
        self.calories = calories
        self.lanes = lanes
    }
}

This might help you.

0
Downgoat On

This is how I'm tackling this in my application: anything that needs to be "variant" (different depending on the type) is put into a struct that is enum'd upon. This isn't great because it means you can't filter/search on the variant properties, but it works:

@Model class Workout {
    var startTime: Date
    var endTime: Date?
    var activities: [Activity]
    
    init(startTime: Date, endTime: Date? = nil, activities: [Activity]) {
        // ...
    }
}

@Model class Activity {
    var calories: Int
    var activityType: ActivityType

    init(calories: Int, activityType: ActivityType) {
        // ...
    }
}

enum ActivityType: Codable {
    case swim(lanes: Int)
    case run(steps: Int)
}

There are a few things to note:

  • You won't be able to query on a specific ActivityType
  • If you use iCloud container sync, Workout.activities will be unsorted. You'll need some kind of order property on activity to ensure ordering.