1

I've read this and this and spent many hours perusing SO, but I can't seem to figure out how to properly use Decode with an empty array.

Original Code

At first, I had no problems with my code.

Here was my original struct:

struct JobHabit: Codable {
    var name: String
    var description: String
    var assigned: String
    var order: Int
    var category: Category
    var active: Bool
    var altName: String
    var altNameDate: Double
    var altAssigned: String
    var altCategory: Category
    var altOrder: Int

    enum Category: String, Codable {
    
        case dailyJobMorning
        case dailyJobEvening
        case weeklyJob1
        case weeklyJob2
        case jobBonus
    
        case quickPoints
        case jobJar

        case dailyHabit
        case weeklyHabit
        case habitBonus
    }
}

And here was my function:

static func observeJobs(completion: @escaping () -> Void) {
    
    FB.ref
        .child(FB.jobs)
        .observe(.value, with: { (snapshot) in

            guard snapshot.exists() else {
                completion()
                return
            }
            
            for item in snapshot.children {
                
                guard let snap = item as? DataSnapshot else { return }
                guard let value = snap.value as? [String: Any] else { return }
                
                do {
                    let jsonData = try JSONSerialization.data(withJSONObject: value, options: [])
                    let habit = try JSONDecoder().decode(JobHabit.self, from: jsonData)
                    
                    jobsMasterList.append(habit)
                    
                } catch let error {

                    print(error)
                    manuallyDecodeJobHabitAndAddToArray(.dailyJobMorning, value)
                }
            }
            
            completion()
        })
}

That all worked great. No problems.

But then...

I added in another parameter to the struct. I added in a points parameter that could potentially be empty. It's not optional, but it could be empty. (It's the last parameter.)

Like this:

struct JobHabit: Codable {
    var name: String
    var description: String
    var assigned: String
    var order: Int
    var category: Category
    var active: Bool
    var altName: String
    var altNameDate: Double
    var altAssigned: String
    var altCategory: Category
    var altOrder: Int
    var points: [Point]    // <=== new parameter that messed everything up
}

And that caused the decode function to fail with this error:

keyNotFound(CodingKeys(stringValue: "points", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "points", intValue: nil) ("points").", underlyingError: nil))

The error makes sense. The decoder can't find a value. Well, if it's empty, then Firebase will return nothing. So that's expected behavior. But why can't the decoder account for that?

I read up on decoding optional values and came up with an initializer for the struct, like so:

struct JobHabit: Codable {
    var name: String
    var description: String
    var assigned: String
    var order: Int
    var category: Category
    var active: Bool
    var altName: String
    var altNameDate: Double
    var altAssigned: String
    var altCategory: Category
    var altOrder: Int
    var points: [Point]

    init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        self.name = try container.decode(String.self, forKey: .name)
        self.description = try container.decode(String.self, forKey: .description)
        self.assigned = try container.decode(String.self, forKey: .assigned)
        self.order = try container.decode(Int.self, forKey: .order)
        self.category = try container.decode(Category.self, forKey: .category)
        self.active = try container.decode(Bool.self, forKey: .active)
        self.altName = try container.decode(String.self, forKey: .altName)
        self.altNameDate = try container.decode(Double.self, forKey: .altNameDate)
        self.altAssigned = try container.decode(String.self, forKey: .altAssigned)
        self.altCategory = try container.decode(Category.self, forKey: .altCategory)
        self.altOrder = try container.decode(Int.self, forKey: .altOrder)
        self.points = try container.decodeIfPresent([Point].self, forKey: .points) ?? []
    }
}

That created a new issue. The issue is that I can't create new instances of my struct anywhere else in the app. When I try doing this:

static let job120 = JobHabit(name: " Bills & finances",
                             description: "record receipts\nupdate accounts\npay bills\nfiling (max 1 hour)",
                             assigned: "none",
                             order: 19,
                             category: .weeklyJob1,
                             active: false,
                             altName: " Bills & finances",
                             altNameDate: 0,
                             altAssigned: "none",
                             altCategory: .weeklyJob1,
                             altOrder: 19,
                             points: [])

I get the following error message:

Extra argument 'name' in call

Apparently, the initializer is making it so I can't create new instances of the struct? I'm really struggling to figure out how to decode a potentially empty array from Firebase.

What am I doing wrong? Everything worked fine with decoding BEFORE I added in that empty points array. Once I added in the points parameter, decode couldn't handle it, and so I had to add in the initializers in the struct. Now my other code doesn't work.

Oh, and here is my JSON tree sample:

{
  "-MOESVPtiXtXQh19sWBP" : {
    "active" : true,
    "altAssigned" : "Dad",
    "altCategory" : "dailyJobMorning",
    "altName" : " Morning Job Inspections",
    "altNameDate" : 0,
    "altOrder" : 0,
    "assigned" : "Dad",
    "category" : "dailyJobMorning",
    "description" : "set job timer\nvisually inspect each person's job...",
    "name" : " Morning Job Inspections",
    "order" : 0
  }
}
Phontaine Judd
  • 428
  • 7
  • 17

1 Answers1

3

When you have an API, which can return a struct with either filled or empty value, you must declare this variable as an optional. In other case how are you going to handle it in your code later?

As apple documentation says, an optional is:

A type that represents either a wrapped value or nil, the absence of a value.

Doesn't this make sense?

So to fix existing issues just declare your points variable as [Point]?. If there is no value for points presented in your API response, it will remain nil. Later in your code use standard optionals' unwrappers to check whether there is value presented or not.

grigorevp
  • 631
  • 6
  • 13
  • I see. I will try this and get back to you. – Phontaine Judd Dec 11 '20 at 06:53
  • Okay, so I changed the points variable to be an optional. I did as you suggested and changed `[Point]` into `[Point]?`. It appears to be working. Wow! What a simple solution. I was hoping it was something that simple. Thanks! I'll keep you posted if there are any more problems. – Phontaine Judd Dec 11 '20 at 06:57