Reputation: 3054
Apologies for the long question.
I am using Firestore to store online data and have a current structure like the below;
{
"activities": {
"mG47rRED9Ym4dkXinXrN": {
"createdAt": 1234567890,
"activityType": {
"title": "Some Title"
}
},
"BF3jhINa1qu9kia00BeG": {
"createdAt": 1234567890,
"activityType": {
"percentage": 50,
}
}
}
}
I am using JSON Decodable Protocol to retrieve the data. I have a main struct of;
struct Activity: Decodable {
let documentID: String
let createdAt: Int
let activityType: ActivityType
}
this struct holds the mandatory data such as createdAt & documentID (i.e. "mG47rRED9Ym4dkXinXrN"). Depending upon the data nested inside "activityType" it should return one of two structs listed below;
struct NewGoal: Decodable {
let title: String
}
struct GoalAchieved: Decodable {
let percentage: Double
}
I am doing this with decodable enumeration;
enum ActivityType: Decodable {
case newGoal(NewGoal)
case goalAchieved(GoalAchieved)
}
extension ActivityType {
private enum CodingKeys: String, CodingKey {
case activityType
}
init(from decoder: Decoder) throws {
let values = try? decoder.container(keyedBy: CodingKeys.self)
if let value = try? values?.decode(GoalAchieved.self, forKey: .activityType) {
self = .goalAchieved(value)
return
}
if let value = try? values?.decode(NewGoal.self, forKey: .activityType) {
self = .newGoal(value)
return
}
throw DecodingError.decoding("Cannot Decode Activity")
}
}
When using the Activity struct as my array I am getting DecodingError. However, when using ActivityType as my array it will decode fine but it will not give access to the documentID & createdAt. I cannot inherit Activity struct as it is non-protocol. How would I go about structuring this, please?
Upvotes: 1
Views: 881
Reputation: 3597
This was kind of tricky and fun to figure out. We have three complications that make this tough:
Here is my solution. It's a bit long. Let's start with your activity struct:
struct Activity {
let documentId: String
let createdAt: Int
let activityType: ActivityType
}
Nice and easy. Now for that top-level decoding container:
struct Activities: Decodable {
let activities: [Activity]
init(from decoder: Decoder) throws {
var activities: [Activity] = []
let activitiesContainer = try decoder.container(keyedBy: CodingKeys.self)
let container = try activitiesContainer.nestedContainer(keyedBy: VariableCodingKeys.self, forKey: .activities)
for key in container.allKeys {
let activityContainer = try container.nestedContainer(keyedBy: ActivityCodingKeys.self, forKey: key)
let createdAt = try activityContainer.decode(Int.self, forKey: .createdAt)
let activityType = try activityContainer.decode(ActivityType.self, forKey: .activityType)
let activity = Activity(
documentId: key.stringValue,
createdAt: createdAt,
activityType: activityType)
activities.append(activity)
}
self.activities = activities
}
private enum CodingKeys: CodingKey {
case activities
}
private struct VariableCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
return nil
}
}
private enum ActivityCodingKeys: CodingKey {
case createdAt, activityType
}
}
You'll notice a couple interesting points:
ActivityCodingKeys
has only two of the fields in the Activity
struct. That's because documentId
is populated with the key of the nested container, which contains the rest of the data.VariableCodingKeys
, which let us work with any key/documentId
. Finally, we have the ActivityType
enum:
enum ActivityType: Decodable {
case newGoal(String), achievedGoal(Double)
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let title = try? container.decode(String.self, forKey: .title) {
self = .newGoal(title)
} else if let percentage = try? container.decode(Double.self, forKey: .percentage) {
self = .achievedGoal(percentage)
} else {
throw DecodingError.keyNotFound(
CodingKeys.title,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Expected title or percentage, but found neither."))
}
}
private enum CodingKeys: CodingKey {
case title, percentage
}
}
One thing that surprised me as I was writing this up is that not all the CodingKeys have to be present for the decoder to generate the keyed container. I used that to combine title
and percentage
in one enum. Like your solution, I try
decoding a certain key, see if it works, and move on if not.
I'll be the first to admit that this solution is not short. It does work, though, and it is kind of cool how it all works out. If you have any questions or ideas for making it more concise, let me know!
Upvotes: 2