skip to Main Content

First of all, i am very sorry for the noob question, but i just cant seem to figure this out.

I am very new to coding and just started to get my feet wet with SwiftUI, following a few courses and started to dabble in trying to create some basic apps.

I am currently working on an app that does an API call and displays the data.

My issue is, im trying to put the decoded data into an array, it sounds so simple and I think i am missing something very easy, but for the life of me I cant seem to figure it out.

Below is the codable struct I have

struct Drinks: Codable, Identifiable {
    let id = UUID()
    let strDrink : String
    let strInstructions: String
    let strDrinkThumb: String?
    let strIngredient1: String?
    let strIngredient2: String?
    let strIngredient3: String?
    let strIngredient4: String?
    let strIngredient5: String?
}

I want to put the ingredients into an Array so I can go through them in lists etc

import SwiftUI

struct IngredientView: View {
    let drink : Drinks
    let ingredientArray : [String] = [] // I want to append the ingredients here
    var body: some View {
        GroupBox() {
            DisclosureGroup("Drink Ingredience") {
                ForEach(0..<3) { item in
                    Divider().padding(.vertical, 2)
                    HStack {
                        Group {
                            // To use the array here
                        }
                        .font(Font.system(.body).bold())
                        Spacer(minLength: 25)
                    }
                }
            }
        }
    }
}

Again, sorry for the noob question that probably has a simple answer, but worth a shot asking 😀

Thanks!

3

Answers


  1. change you struct to

    struct Drink: Codable, Identifiable {
        let id = UUID()
        let strDrink : String
        let strInstructions: String
        let strDrinkThumb: String?
        let strIngredients: [String] = []
    }
    

    wherever you want to loop through ingredients you can use drink.strIngredients array

    Login or Signup to reply.
  2. you could use this approach to get all your ingredients into an array and use
    it in Lists. The idea is to use a function to gather all your ingredients into an array of
    Ingredient objects. You could also use a computed property.
    It is best to use a Ingredient object and declare it Identifiable
    so that when you use them in list and ForEach, each one will be
    unique, even if the names are the same.

    import SwiftUI
    
    @main
    struct TestApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
    struct ContentView: View {
        @State var drinkList = [Drink]()
        
        var body: some View {
            List {
                ForEach(drinkList) { drink in
                    VStack {
                        Text(drink.strDrink).foregroundColor(.blue)
                        Text(drink.strInstructions)
                        ForEach(drink.allIngredients()) { ingr in
                            HStack {
                                Text(ingr.name).foregroundColor(.red)
                                Text(ingr.amount).foregroundColor(.black)
                            }
                        }
                    }
                }
            }
            .task {
                let theResponse: ApiResponse? = await getData(from: "https://www.thecocktaildb.com/api/json/v1/1/search.php?s=margarita")
                if let response = theResponse {
                    drinkList = response.drinks
                }
           }
        }
        
        func getData<T: Decodable>(from urlString: String) async -> T? {
            guard let url = URL(string: urlString) else {
                print(URLError(.badURL))
                return nil // <-- todo, deal with errors
            }
            do {
                let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
                guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                    print(URLError(.badServerResponse))
                    return nil // <-- todo, deal with errors
                }
                return try JSONDecoder().decode(T.self, from: data)
            }
            catch {
                print("---> error: (error)")
                return nil // <-- todo, deal with errors
            }
        }
        
    }
    
    struct ApiResponse: Decodable {
        var drinks: [Drink]
    }
        
    struct Drink: Decodable, Identifiable {
        let id = UUID()
        let idDrink: String
        let strDrink: String
        let strDrinkThumb: String
        let strAlcoholic: String
        let strGlass: String
        let strInstructions: String
        
        let strIngredient1: String?
        let strIngredient2: String?
        let strIngredient3: String?
        let strIngredient4: String?
        let strIngredient5: String?
        let strIngredient6: String?
        let strIngredient7: String?
        let strIngredient8: String?
        let strIngredient9: String?
        let strIngredient10: String?
        
        var strMeasure1: String?
        var strMeasure2: String?
        var strMeasure3: String?
        var strMeasure4: String?
        var strMeasure5: String?
        var strMeasure6: String?
        var strMeasure7: String?
        var strMeasure8: String?
        var strMeasure9: String?
        var strMeasure10: String?
        
        // --- here adjust to your needs, could also use a computed property
        func allIngredients() -> [Ingredient] {
          return [
            Ingredient(name: strIngredient1 ?? "", amount: strMeasure1 ?? ""),
            Ingredient(name: strIngredient2 ?? "", amount: strMeasure2 ?? ""),
            Ingredient(name: strIngredient3 ?? "", amount: strMeasure3 ?? ""),
            Ingredient(name: strIngredient4 ?? "", amount: strMeasure4 ?? ""),
            Ingredient(name: strIngredient5 ?? "", amount: strMeasure5 ?? ""),
            Ingredient(name: strIngredient6 ?? "", amount: strMeasure6 ?? ""),
            Ingredient(name: strIngredient7 ?? "", amount: strMeasure7 ?? ""),
            Ingredient(name: strIngredient8 ?? "", amount: strMeasure8 ?? ""),
            Ingredient(name: strIngredient9 ?? "", amount: strMeasure9 ?? ""),
            Ingredient(name: strIngredient10 ?? "", amount: strMeasure10 ?? "")
        ].filter{!$0.name.isEmpty}
      }
    
    }
    
    struct Ingredient: Identifiable {
        let id = UUID()
        var name: String
        var amount: String
    }
    
    Login or Signup to reply.
  3. First of all your design cannot work because you are ignoring the root object, a struct with a key drinks

    struct Root : Decodable {
        let drinks : [Drink]
    }
    

    A possible solution is to write a custom init method. However this a bit tricky because you have to inject dynamic CodingKeys to be able to decode the ingredients in a loop

    First create a custom CodingKey struct

    struct AnyKey: CodingKey {
        var stringValue: String
        var intValue: Int?
        
        init?(stringValue: String) {  self.stringValue = stringValue  }
        init?(intValue: Int) { return nil } // will never be called
    }
    

    In the Drink struct – by the way it’s supposed to be named in singular form – specify a second container, create the ingredient keys on the fly, decode the ingredients in a loop and append the results to an array until the value is nil. Somebody should tell the owners of the service that their JSON structure is very amateurish. As you have to specify the CodingKeys anyway I mapped the keys to more meaningful and less redundant struct member names.

    struct Drink: Decodable, Identifiable {
        
        private enum CodingKeys : String, CodingKey {
            case name = "strDrink", instructions = "strInstructions", thumbnail = "strDrinkThumb"
        }
        
        let id = UUID()
        let name: String
        let instructions: String
        let thumbnail: URL
        let ingredients: [String]
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.name = try container.decode(String.self, forKey: .name)
            self.instructions = try container.decode(String.self, forKey: .instructions)
            self.thumbnail = try container.decode(URL.self, forKey: .thumbnail)
            var counter = 1
            var temp = [String]()
            let anyContainer = try decoder.container(keyedBy: AnyKey.self)
            while true {
                let ingredientKey = AnyKey(stringValue: "strIngredient(counter)")!
                guard let ingredient = try anyContainer.decodeIfPresent(String.self, forKey: ingredientKey) else { break }
                temp.append(ingredient)
                counter += 1
            }
            ingredients = temp
        }
    }
    

    In the view display the ingredients like this

    struct IngredientView: View {
        let drink : Drink
     
        var body: some View {
            GroupBox() {
                DisclosureGroup("Drink Ingredience") {
                    ForEach(drink.ingredients) { ingredient in
                        Divider().padding(.vertical, 2)
                        HStack {
                            Group {
                                Text(ingredient)
                            }
                            .font(Font.system(.body).bold())
                            Spacer(minLength: 25)
                        }
                    }
                }
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search