I believe I found a solution to this. The error I was getting was caused by getting the container for the JSON data too many times, using up the data out of step of my intent. I guess it has something to to with how data is streamed from the file into the decoder.
The way out is ...
To break encapsulation a bit by making the CheckTask super class aware of all keys of of any of it's subclasses so that when the decoding process gets a nestedContainer, it's ready to go with all key knowledge whether it uses them all or not. that means the same container can be used for the decoding of any subclass.
and
Each subclass implements an extra init function that takes that container as an argument, then specifically decodes from there.
Here are some pertinent code snippets...
class CheckList: Identifiable, ObservableObject, Codable {
let id: String
var title: String
@Published var tasks: [CheckTask]
var domain: String
....
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
title = try container.decode(String.self, forKey: .title)
domain = try container.decode(String.self, forKey: .domain)
// get the CheckTasks even though we don't know what each one is yet
var tasksContainer = try container.nestedUnkeyedContainer(forKey: .tasks)
tasks = []
// Iterate through the array of objects, determine the type, and decode accordingly
while !tasksContainer.isAtEnd {
// Because we don't know which subclass yet, we toss all possible keys at the taskContainer.
// We only get one bite at the apple and it has to be prepared to decode anything.
// See CheckTask.CodingKeys for more info.
let taskContainer = try tasksContainer.nestedContainer(keyedBy: CheckTask.CodingKeys.self)
let type = try taskContainer.decode(CheckType.self, forKey: .type)
// Now that we know the type, we choose how to init the correct class,
// using a convenience method that takes a container,
// then append to the array of tasks.
switch type {
case .CheckItem:
let item = try CheckItem(container: taskContainer)
tasks.append(item)
default:
// Handle unknown type or log a warning
print("Unknown task type:.")
}
}
}
}
class CheckTask: Identifiable, ObservableObject, Equatable, Codable {
var id: UUID
var locked: Bool
var type: CheckType
....
// CodingKeys to specify custom mapping between Swift names and JSON keys
// I hate to break encapsulation like this but, hey, that's decoding for ya.
enum CodingKeys: String, CodingKey {
// common keys
case id, locked, type
// CheckItem keys
case title, checkmark, skippable, complete, instructions, instructionLinks
// CheckGroup keys
case items
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
locked = try container.decode(Bool.self, forKey: .locked)
type = try container.decode(CheckType.self, forKey: .type)
}
}
enum CheckType: Codable {
case CheckItem
case CheckGroup
case Unknown
}
class CheckItem: CheckTask {
var title: String
@Published var checkmark: CheckMark
var skippable: Bool = false
var instructions: String
var instructionLinks: [InstructionLink]
....
// Initialize with values
init(id: UUID, title: String, checkmark: CheckMark, skippable: Bool, locked: Bool, instructions: String?, instructionLinks: [InstructionLink]?) {
self.title = title
self.checkmark = checkmark
self.skippable = skippable
self.instructions = instructions ?? ""
self.instructionLinks = instructionLinks ?? []
super.init(id: id, locked: locked, type: CheckType.CheckItem)
}
/// Custom decoding method to decode the properties from a JSON container. This is used by the CheckList decode
/// so that it can handle instantiating subclasses of CheckTask blindly since it doesn't know there are subclasses
/// and can't direct specify a type.
/// - Parameter container: <#container description#>
convenience init(container: KeyedDecodingContainer<CodingKeys>) throws {
self.init(id: try container.decode(UUID.self, forKey: .id),
title: try container.decode(String.self, forKey: .title),
checkmark: try container.decode(CheckMark.self, forKey: .checkmark),
skippable: try container.decode(Bool.self, forKey: .skippable),
locked: try container.decode(Bool.self, forKey: .locked),
instructions: try container.decode(String.self, forKey: .instructions),
instructionLinks: try container.decode([InstructionLink].self, forKey: .instructionLinks)
)
}
}
So now I'll implement another subclass of CheckTask called CheckGroup and see how I can break stuff then.