WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Trying to get API to change based on navigation link selection

Forums > SwiftUI

Hey,

So I am tryingn to get my recipeView to change based on the navigation link selection on the home page-- for example, selecting vegan should load the Vegan recipes from the vegan api link, the breakfast should load only the breakfast api link. But the problem I am having is switching the recipe list view api based on their selection in the contentView from the navigaiton links.

Content View

import SwiftUI

struct ContentView: View {
    let items = ["Vegan", "Breakfast", "Lunch", "Dinner"]
    let rows: [GridItem] = [GridItem(.fixed(150))]
    @StateObject var navSelected = NavSelection()

    var body: some View {
        NavigationView {
                    ScrollView {
                        ScrollView(.horizontal, showsIndicators: false) {
                            LazyHGrid(rows: rows, alignment: .center) {
                                ForEach(items, id: \.self) { item in
                                    NavigationLink {
                                        RecipeList(navSelection: navSelected)
                                    } label: {
                                        VStack(alignment: .leading, spacing: 0) {
                                            Image("navigation/\(item)") // image view here
                                                .frame(width: 103, height: 101)
                                                .cornerRadius(10)
                                                .foregroundColor(.gray)
                                            Text(item)

                                        }
                                    }
                                }
                            }
                            .frame(height: 150)
                        }
                        .padding(.horizontal)
                    }
                }
        .preferredColorScheme(.dark)

}
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Recipes API Call

import SwiftUI

struct Recipes: Identifiable, Codable {
    let id: Int
    let name: String
    let creator: String
    let serves: Int
    let ingredients: [Ingredient]
    let methods: [Method]
    let imageURL: URL

    enum CodingKeys: String, CodingKey {
        case id, name, creator, serves, ingredients
        case methods = "method"
        case imageURL = "imageurl"
    }
}
struct Ingredient: Codable {
    let name: String
    let quantity: Double
    let measurement: String
}

struct Method: Codable {
    let step: Int
    let text: String
}

let veganS3 = "https://recipesstore.s3.eu-west-2.amazonaws.com/vegan.json"
let breakfastS3 = "https://recipesstore.s3.eu-west-2.amazonaws.com/breakfast.json"
let lunchDinnerS3 = "https://recipesstore.s3.eu-west-2.amazonaws.com/lunchdinner.json"

var s3API = ""

@MainActor
class RecipeAPI: ObservableObject {
    @Published var Recipe: [Recipes] = []

    func fetchRecipes() async {

        let apiURL = URL(string: s3API)!
        // Use Enum switch case for URL depedning on what is selected on content View.

        do {
            let (data, _) = try await URLSession.shared.data(from: apiURL)
            let decoder = JSONDecoder()
            Recipe = try decoder.decode([Recipes].self, from: data)
        } catch {
            print(error)
        }
    }
}

class NavSelection: ObservableObject{
    @Published var Selected = [String]()

    func switchAPI(){
     if Selected == ["Vegan"]{
              s3API = veganS3
          }else if Selected == ["Breakfast"]{
              s3API = breakfastS3
          }else if Selected == ["Lunch"] {
              s3API = lunchDinnerS3
          }else if Selected == ["Dinner"] {
              s3API = lunchDinnerS3
          }
    }
}

RecipeListView

import SwiftUI

struct RecipeList: View {
    @StateObject var Api = RecipeAPI()
    @ObservedObject var navSelection: NavSelection

    var body: some View {

        NavigationView {

            VStack{
                Image("Rectangle")
                                .frame(height: 244)

                List{
                    HStack{
                        Text(navSelection.Selected)
                            .font(.title)
                            .fontWeight(.heavy)
                    }

                    ForEach(Api.Recipe){ recipe in
                        NavigationLink {
                            Text(recipe.name)
                        } label: {
                            AsyncImage(url: URL(string: "\(recipe.imageURL)")) { image in
                                image.resizable()
                            } placeholder: {
                                Color.gray
                            }
                            .frame(width: 130, height: 81)

                            VStack(alignment: .leading){
                                Text(recipe.name)
                                    .font(.headline)

                                HStack{
                                    Text(recipe.creator)
                                        .font(.subheadline)
                                }
                            }
                        }
                    }

                }
                .task {
                    await Api.fetchRecipes()
                }

            }
            .ignoresSafeArea(edges: .top)
        }
        .preferredColorScheme(.dark)

    }

}

struct VeganRecipesList_Previews: PreviewProvider {
    static var previews: some View {
        RecipeList()
    }
}

I've proably overcomplicated this alot or messed up somewhere. Please help.

Best, imran

   

You weren't overcomplicated or messed up at all. It just needed some massaging to get it working well.

import SwiftUI

struct Recipe: Identifiable, Codable {
    let id: Int
    let name: String
    let creator: String
    let serves: Int
    let ingredients: [Ingredient]
    let methods: [Method]
    let imageURL: URL

    enum CodingKeys: String, CodingKey {
        case id, name, creator, serves, ingredients
        case methods = "method"
        case imageURL = "imageurl"
    }
}

struct Ingredient: Codable {
    let name: String
    let quantity: Double
    let measurement: String
}

struct Method: Codable {
    let step: Int
    let text: String
}

enum RecipeSelection: String, CaseIterable, Identifiable {
    case vegan
    case breakfast
    case lunch
    case dinner

    var id: String { self.rawValue }
    var name: String { self.rawValue.capitalized }

    var menu: String {
        switch self {
        case .vegan: return "vegan"
        case .breakfast: return "breakfast"
        case .lunch, .dinner: return "lunchdinner"
        }
    }   
}

@MainActor
class RecipeAPI: ObservableObject {
    @Published var recipes: [Recipe] = []

    //we'll use this URL as a basis for all of the different URLs
    let baseURL = "https://recipesstore.s3.eu-west-2.amazonaws.com/MENU.json"

    func fetchRecipes(for selection: RecipeSelection) async {

        //create the API URL by substituting the selected menu
        //into the baseURL
        let apiURL = URL(string: baseURL.replacingOccurrences(of: "MENU", with: selection.menu))!

        do {
            let (data, _) = try await URLSession.shared.data(from: apiURL)
            let decoder = JSONDecoder()
            recipes = try decoder.decode([Recipe].self, from: data)
        } catch {
            print(error)
        }
    }
}

struct RecipeList: View {

    let menu: RecipeSelection

    @StateObject var api = RecipeAPI()

    var body: some View {
        List {
            ForEach(api.recipes) { recipe in
                VStack {
                    Text(recipe.name)
                }
            }
        }
        .task {
            await api.fetchRecipes(for: menu)
        }
        .navigationTitle("Recipes")
    }
}

struct RecipeContentView: View {
    let rows: [GridItem] = [GridItem(.fixed(150))]

    var body: some View {
        NavigationView {
            ScrollView {
                ScrollView(.horizontal, showsIndicators: false) {
                    LazyHGrid(rows: rows, alignment: .center) {
                        ForEach(RecipeSelection.allCases) { menu in
                            NavigationLink {
                                RecipeList(menu: menu)
                            } label: {
                                VStack(alignment: .leading, spacing: 0) {
                                    Image("navigation/\(menu)") // image view here
                                        .frame(width: 103, height: 101)
                                        .cornerRadius(10)
                                        .foregroundColor(.gray)
                                    Text(menu.name)

                                }
                            }
                        }
                    }
                    .frame(height: 150)
                }
                .padding(.horizontal)
            }
        }
        .preferredColorScheme(.dark)

    }
}

Some notes:

  1. RecipeAPI: The fetchRecipes method now takes a parameter to indicate which menu the user has selected.
  2. RecipeAPI: I consolidated the various URLs into a single one that is changed based on the user's selection. The only difference between all of them was the particular menu, so the baseURL is adjusted using replacingOccurrences(of:with:) to get the appropriate URL. And I included the baseURL into RecipeAPI rather than having it as a global constant.
  3. RecipeAPI: I renamed the recipe property to recipes and its type from [Recipes] to [Recipe]. The property holds a list of recipes. You should therefore name the property in the plural. Arrays hold a list of single items, so the type should be singular.
  4. RecipeSelection: I created an enum for the various menu choices the user can select from. It conforms to CaseIterable and Identifiable so we can loop through them in ForEach. It conforms to RawRepresentable with a String base type so we can use its string representation as an id (for looping in ForEach) and also get a formatted string name for display to the user.
  5. The NavSelected object was unnecessary since we have the RecipeSelection enum.
  6. All other changes should clearly result from the above changes.

Any questions, just ask.

   

But then how is this connected to the recipe view itself meaning the recipe that needs to be displayed inside of a view when clicked?

   

Hacking with Swift is sponsored by Fernando Olivares

SPONSORED Fernando's book will guide you in fixing bugs in three real, open-source, downloadable apps from the App Store. Learn applied programming fundamentals by refactoring real code from published apps. Hacking with Swift readers get a $10 discount!

Read the book

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.