In my app (which utilizes SwiftUI, SwiftData and CloudKit), I have the following class and enum:
@Model
class CategoryType
{
var name: String = ""
var parameterType: CategoryTypeParameterType = CategoryTypeParameterType.none
init(name: String, parameterType: CategoryTypeParameterType)
{
self.name = name
self.parameterType = parameterType
}
}
enum CategoryTypeParameterType: Codable, CaseIterable, Hashable
{
case none
case number(number: UInt)
case range(range: ClosedRange<UInt>)
var name: String
{
switch self
{
case .none:
return "None"
case .number:
return "Number"
case .range:
return "Range"
}
}
static var allCases: [CategoryTypeParameterType]
{
return [.none,
.number(number: 1),
.range(range: 1...10)]
}
}
Simply creating a CategoryType
object that has a parameterType
with the value CategoryTypeParameterType.range
crashes the program with the following:
Thread 1: "[<__SwiftValue 0x301467570> valueForUndefinedKey:]: this class is not key value coding-compliant for the key lowerBound.
I'm creating the object like so:
let book = ProjectType(name: "Book")
self.modelContext.insert(projectType)
book.categoryTypes?.append(CategoryType(name: "Pages", parameterType: CategoryTypeParameterType.range(range: 1...10)))
and here is the ProjectType
class for reference:
@Model
class ProjectType
{
var name: String = ""
@Relationship(deleteRule: .cascade, inverse: \CategoryType.projectTypes) var categoryTypes: [CategoryType]?
var createdDate: Date = Date.now
init(name: String)
{
self.name = name
self.categoryTypes = []
}
}
Why isn't this working?
What happens is that SwiftData creates a sql table with separate columns for number and the lower and upper bound for the range. But even though it creates such table itself it fails to use it properly when trying to save an object so this is clearly some bug involved here.
So I have no solution for this but a workaround to use for the time being. The workaround is a pattern sometime used in other cases in SwiftData and Core Data and that is to store a base type and then use a computed property to convert between that type and the "public" one.
So first we add a private property of type Data
to CategoryType
private var parameterTypeData: Data
and then the computed property that uses json encoding and decoding to convert between the enum and Data
var parameterType: CategoryTypeParameterType {
get { try! JSONDecoder().decode(CategoryTypeParameterType.self, from: parameterTypeData) }
set { parameterTypeData = try! JSONEncoder().encode(newValue) }
}
I also changed the init so that we give the parameter the default value there and we need to encode that parameter separately.
init(name: String, parameterType: CategoryTypeParameterType = .none) {
self.name = name
parameterTypeData = try! JSONEncoder().encode(parameterType)
}
Note that I use try!
when encoding and decoding and it should work fine since the parameterTypeData
property is private so no other values can be set than what exists in the enum but you might want to change this if you want.
Also note that this might not be ideal from a performance perspective with all the encoding and decoding and creating objects for that even though some improvements can be made.