Search code examples
iosswiftdecode

How to decode dynamic keys using prefix


I want to decode strIngredient1, strIngredient2, strIngredient3 to an array and strMeasure1, strMeasure2, strMeasure3 to a separate array. How can we do this using decodable and Coding keys. I have an object that conforms to the decodable protocol and a function public init(from decoder: Decoder)

{
  "meals": [
    {
      "idMeal": "53049",
      "strMeal": "Apam balik",
      "strDrinkAlternate": null,
      "strCategory": "Dessert",
      "strArea": "Malaysian",
      "strInstructions": "Mix milk, oil and egg together. Sift flour, baking powder and salt into the mixture. Stir well until all ingredients are combined evenly.\r\n\r\nSpread some batter onto the pan. Spread a thin layer of batter to the side of the pan. Cover the pan for 30-60 seconds until small air bubbles appear.\r\n\r\nAdd butter, cream corn, crushed peanuts and sugar onto the pancake. Fold the pancake into half once the bottom surface is browned.\r\n\r\nCut into wedges and best eaten when it is warm.",
      "strMealThumb": "https://www.themealdb.com/images/media/meals/adxcbq1619787919.jpg",
      "strTags": null,
      "strYoutube": "https://www.youtube.com/watch?v=6R8ffRRJcrg",
      "strIngredient1": "Milk",
      "strIngredient2": "Oil",
      "strIngredient3": "Eggs",
      "strIngredient4": "Flour",
      "strIngredient5": "Baking Powder",
      "strIngredient6": "Salt",
      "strIngredient7": "Unsalted Butter",
      "strIngredient8": "Sugar",
      "strIngredient9": "Peanut Butter",
      "strIngredient10": "",
      "strIngredient11": "",
      "strIngredient12": "",
      "strIngredient13": "",
      "strIngredient14": "",
      "strIngredient15": "",
      "strIngredient16": "",
      "strIngredient17": "",
      "strIngredient18": "",
      "strIngredient19": "",
      "strIngredient20": "",
      "strMeasure1": "200ml",
      "strMeasure2": "60ml",
      "strMeasure3": "2",
      "strMeasure4": "1600g",
      "strMeasure5": "3 tsp",
      "strMeasure6": "1/2 tsp",
      "strMeasure7": "25g",
      "strMeasure8": "45g",
      "strMeasure9": "3 tbs",
      "strMeasure10": " ",
      "strMeasure11": " ",
      "strMeasure12": " ",
      "strMeasure13": " ",
      "strMeasure14": " ",
      "strMeasure15": " ",
      "strMeasure16": " ",
      "strMeasure17": " ",
      "strMeasure18": " ",
      "strMeasure19": " ",
      "strMeasure20": " ",
      "strSource": "https://www.nyonyacooking.com/recipes/apam-balik~SJ5WuvsDf9WQ",
      "strImageSource": null,
      "strCreativeCommonsConfirmed": null,
      "dateModified": null
    }
  ]
}

Solution

  • Here's what I would do, which is a little different from the commenter. I would start by creating a custom CodingKey implementation for the two numbered fields:

    enum MealNumberedFieldKey {
        case ingredients(Int)
        case measurements(Int)
    }
    
    extension MealNumberedFieldKey: CodingKey {
        var intValue: Int? { nil }
    
        init?(intValue: Int) {
            return nil
        }
    
        init?(stringValue: String) {
            var string = stringValue
            if string.hasPrefix("strIngredient") {
                string.removeFirst(13)
                guard let number = Int(string) else { return nil }
                self = .ingredients(number)
            } else if string.hasPrefix("strMeasure") {
                string.removeFirst(10)
                guard let number = Int(string) else { return nil }
                self = .measurements(number)
            } else {
                return nil
            }
        }
    
        var stringValue: String {
            switch self {
            case .ingredients(let number):
                return "strIngredient\(number)"
            case .measurements(let number):
                return "strMeasure\(number)"
            }
        }
    }
    

    Then I would write a custom init(from decoder:) function using two decoding container, one with the normal coding keys, and another using the custom coding keys:

    struct Meal: Decodable {
        let idMeal: String
        let strMeal: String
        // ...
        let ingredients: [String]
        let measurements: [String]
    
        enum CodingKeys: String, CodingKey {
            case idMeal
            case strMeal
        }
    
        init(from decoder: any Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.idMeal = try container.decode(String.self, forKey: .idMeal)
            self.strMeal = try container.decode(String.self, forKey: .strMeal)
            // ...
    
            let numberedContainer = try decoder.container(keyedBy: MealNumberedFieldKey.self)
            self.ingredients = try (1...20).map { number in
                try numberedContainer.decode(String.self, forKey: .ingredients(number))
            }
            self.measurements = try (1...20).map { number in
                try numberedContainer.decode(String.self, forKey: .measurements(number))
            }
        }
    }