BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

SOLVED: Dynamic Filtering for API data in list view for my app based on date

Forums > SwiftUI

Hi,

I uplaod new recipes to my DB every week, the JSONs have a date added attached to it. What I am trying to do is filter out recipes upto this weeks begnning (i.e Every Monday for example) recipes to show and anything ahead in the json to not show in the list until it has too. So for example, you should see all the recipes tagged with 9 May 22 on the list but no recipes tagged 16 may 22 for example.

I have multiple List views all showing different Recipes catgeory APIs, so I am asusming I will put it in the recipe lit view rather than the actual API call area but I don't really know how?

Here is my Recipe API:

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

    //we'll use this URL as a basis for all of the different URLs
    let baseURL = "XXXX"

    func fetchRecipes(for selection: RecipeSelection) async {

        //create the API URL by substituting the selected menu
        //into the baseURL
        guard let apiURL = URL(string: baseURL.replacingOccurrences(of: "MENU", with: selection.menu)) else {
            print("Invalid URL")
            return
        }

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

        } catch {
            print(error)
        }
    }
}

Here is my Recipe List View:

struct RecipeList: View {
    let menu: RecipeSelection

    @StateObject var api = RecipeAPI()

    var body: some View {

        List {
            //HeaderImage
            Image("HeaderImages/\(menu.name)")
                .resizable()
                .frame(height: 250)
                .aspectRatio(contentMode: .fill)
                .listRowInsets(EdgeInsets(.zero))
                .padding(.bottom, 10)
                .listRowBackground(Color("backColor"))

            //View Discription and Title Header
            Text(menu.name)
                .font(.largeTitle)
                .fontWeight(.bold)
                .listRowBackground(Color("backColor"))
                .listRowSeparator(.hidden)

            Text("Explore \(menu.name) recipes from creators you love.")
                .listRowBackground(Color("backColor"))

            //List of Recipes
            ForEach(api.recipes) { recipe in
                NavigationLink(destination: RecipesLanding(recipe: recipe)){
                    HStack{
                        AsyncImage(url: URL(string: "\(recipe.imageURL)")) { image in
                            image
                                .resizable()
                                .cornerRadius(8)
                                .frame(width: 130, height: 81)
                                .clipped()
                                .aspectRatio(contentMode: .fit)
                        } placeholder: {
                            Rectangle()
                                .fill(Color.gray)
                                .frame(width: 130, height: 81)
                                .cornerRadius(8)
                        }

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

            }
            .listRowBackground(Color("backColor"))
            .padding(5)

        }
        .listRowBackground(Color("backColor"))
        .frame( maxWidth: .infinity)
        .edgesIgnoringSafeArea(.all)
        .listStyle(GroupedListStyle())

        .task {
            await api.fetchRecipes(for: menu)
        }
        .background(Color("backColor"))

    }
}

Hope you can help-- also if you can everytime I run the app the background color I have added to the view for some reason the bottom of the screen when you reach the bottom of the scroll is white not the color I have chosen for the app-- it works fine for the rest of the app just in this list view.

Best, Imran

   

One solution would be to change your fetchRecipes function like this:

func fetchRecipes(for selection: RecipeSelection, asOf date: Date? = nil) async {
   ...
}

And when you call it you would pass in a Date if you want to filter by that Date or nil if you want to return all the recipes. If you are always going to want to filter the recipes, you could eliminate the optionality of asOf and the default nil parameter so that you would be required to pass in a Date.

Then, instead of directly assigning the decoded JSON to recipes:

recipes = try decoder.decode([Recipe].self, from: data)

do something like this to filter them by date, if an asOf date was supplied:

let allRecipes = try decoder.decode([Recipe].self, from: data)

if let asOfDate = date {
    recipes = allRecipes.filter { 
        //put the code to filter by date here
    }
} else {
    date == nil so we want all recipes
    recipes = allRecipes
}

Note: Since I have no idea what your Recipe data type looks like, I couldn't supply appropriate code for the filter. But the gist of it is that you would compare asOfDate to whatever date property is in Recipe and return true if the recipe should be displayed, false if it should not.

   

So in terms of as of date. Would I just put Date.now for the filtering so for example I want to filter any dateAdded <= to Date.now because that would make sure anything I have added to the JSON that is for say next week or the week after wont show.

   

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

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

Sure, that would work

//assuming Recipe.dateAdded is of type Date
recipes = allRecipes.filter { $0.dateAdded <= Date.now }

Remember that Date includes a time as well, so things that were posted the same day but later in time will also be filtered out. If that's what you want, cool! If not, you will need to do some work with Calendar to just compare the date part.

   

Imran and Patrick are discussing strategy:

Imran: What I am trying to do is filter out recipes upto this weeks ....
Patrick: One solution would be to change your fetchRecipes function like this.... snip ....
Imran: Would I just put Date.now for the filtering so for example....
Patrick: Sure, that would work...

I hope you are getting closer to your goal. You are getting good advice!

One thing I wanted to point out, perhaps part of your original question? How should you obtain recipes for a user selected date range. One of the clues you provided is that you upload new recipes to your database each week.

This might boil down to a fundamental business question, How many total recipes do you have? Maybe you have enough recipes to fill a small book? Or maybe you more than enough to fill a regional library?

Now your question becomes an architecture question!

Approach 1: Download everything!

If you have a reasonable number of recipes, perhaps you can consider downloading all of them. Download them all as JSON each and every time your user launches the application.

Then you'll have your recipes in a local collection and can use common filtering techniques to extract subsets from your collection. You'll have to run tests to determine some point where it's too costly in terms of waiting time to download the entire library of recipes.

Approach 2: Download Summaries

As your database gets larger, you may want, instead, to just download summaries. Download names, categories, ratings, a graphic, and a brief summary of your recipes. Again, once this is downloaded to a local collection, you can use common filtering techniques as discussed in Patrick's messages.

When your user wants more detail, they can click a "Show me" button, which will execute a new database query asking for the details: ingredients, instructions, and step-by-step photos.

Approach 3: Filter Selections in the Database.

If you're already using a database, no explanation necessary. You know that databases are tuned for selecting subsets. In this case, you ask your user to select parameters (carnivore dishes, moderate price, Jamaican Grill) and you ask your database to perform a query to limit the selection before packaging as JSON and sending down to your application. This allows the data to safely stay in your database, and you'll send just limited data throught the network to your user's iOS application.

This might be a good strategy if you have very large libraries of recipes.

Best Approach?

This is a business decision! You'll probably need to experiment with very large libraries of recipes. Conduct experiments to determine how long is a reasonable amount of time to wait for recipes to download. Also consider your user's phone! Do they want megabytes of soup recipes if they're mostly interested in seafood? Where is the tradeoff?

There is no one best approach. You have lots to consider!

   

What is the best way to format the date and then filter it. This is my API Fetching call. What I am confused about is how I can filter out my recipes to the date as mentioned above, except the date is in the JSON is just 18/05/2022 not conformant to Date in swiftui. So where in my code exactly do I convert the string into the correct date format?

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

    func fetchRecipes() async {

        guard let breakfastAPIURL = URL(string: URLData.breakfast.rawValue) else {
            fatalError("Missing URL") 
        }

          do {
              let (data, _) = try await URLSession.shared.data(from: breakfastAPIURL)
              let decoder = JSONDecoder()
              recipes = try decoder.decode([Recipe].self, from: data)
              //recipes = recipes.filter { $0.dateAdded <= Date.now }

          } catch {
              print("Error decoding: ", error)
          }
      }

}

Sorry I am a visual learner, so I struggle without knowing where.

   

How you approach this depends on whether or not all dates fields in your JSON are formatted like 18/05/2022 or if there are other dates that are formatted differently.

If every date in the JSON is formatted like dd/MM/yyyy then you can set the decoder's dateDecodingStrategy to .formatted() and supply a DateFormatter object set up to handle dd/MM/yyyy.

A quick and dirty example you can test in a playground:

import Foundation

let dateJSON = """
{
    "item" : "thing",
    "date": "18/05/2022"
}
"""

struct JSONDateTester: Decodable {
    let item: String
    let date: Date
}

do {
    //create a DateFormatter
    let formatter = DateFormatter()
    //and tell it what format we are expecting to receive
    formatter.dateFormat = "dd/MM/yyyy"

    let decoder = JSONDecoder()
    //now use the DateFormatter as the dateDecodingStrategy
    decoder.dateDecodingStrategy = .formatted(formatter)

    let response = try decoder.decode(JSONDateTester.self, from: dateJSON.data(using: .utf8)!)
    print(response)  //JSONDateTester(item: "thing", date: 2022-05-18 07:00:00 +0000)
} catch {
    print(error)
}

This will create the Date object with the supplied date and default the time to 00:00 UTC (My time zone is UTC-7:00, which is why the date result above shows the time that it does.)

If you have several date fields in your JSON with different formats, then things get a little trickier. But let me know if the above solution works for you before I start getting into all that.

   

Hi @ImranRazak1 Thought you had SOLVED but still having some issues. So I thought I might give you a possible solution. There is a lot of code here but you can look at the project on GitHub NigelGee/Cookbook.

Data

I am correct you have at the moment a JSON file for EACH menu type then do a network call when a user select a menu to view so if a user does changes a menu (or if you do the above solutions when filtering it) then it will do a network call EVERY time. The ways I would make the JSON is one says called Menu with the type and recipes in it.

This is what it may look like. (A few things to note the change the id to be a UUID as now all in one file makes keeping tracking of Ints difficult. Add dateCreated and used iso8601 format as it is built in to swift and easier to decode. change imageurl to imageURL so eliminate the use of CodingKeys)

[
    {
        "type": "Vegan", // <- Menu Types for each menu
        "recipes": [
            {
                "id": "46A0344E-71DC-42C7-9222-212A9CAE6578", // <- change this to a UUID
                "name": "Butternut Squash Mac and Cheese",
                "creator": "Joe Wicks",
                "dateCreated": "2022-04-13T00:00:00+01:00", // <- made the date a iso8601 format
                "serves": 1,
                "ingredients": [
                    {
                        "name": "butternut squash, cut into small chunks",
                        "quantity": 100,
                        "measurement": "g"
                    },
                   // ETC
                ],
                "method": [
                    {
                        "step": 1,
                        "text": "Preheat the oven to 180ºC. Place the squash in a microwavable bowl with a splash of water. Cover and microwave for 3-4 minutes until tender then blend with the quark until smooth."
                    },
                    // ETC
                ],
                "imageURL": "https://images.ctfassets.net/izjiv8mj8dix/4VunIGkptWHPXGWUNAjvf7/2170c8842cc83f7e7d36dfbe35b6b389/Butternut_Squash_Mac_and_Cheese.jpg"  
            },
            {
                "id": "AF969238-C612-4A94-A9D7-32147C529506",
                "name": "Broccoli and Cheddar Soup",
                "creator": "Joe Wicks",
                "dateCreated": "2022-05-13T00:00:00+01:00",
                "serves": 1,
                "ingredients": [
                    {
                        "name": "olive oil",
                        "quantity": 1,
                        "measurement": "tsp"
                    },
                  // ETC
                ],
                "method": [
                    {
                        "step": 1,
                        "text": "Heat the oil in a saucepan, add the garlic and fry for a minute until softened. Stir in the lentils, pour over enough boiling water to cover by a couple of centimetres then bring to the boil and cook for 15 minutes."
                    },
                    // ETC
                ],
                "imageURL": "https://images.ctfassets.net/izjiv8mj8dix/72yr26ZzNN3jdEvyCdsMxn/9047c8632e373275b4e7924ed921e9fe/Broccoli_and_Cheddar_Soup.jpg"
            }
        ]
    },
    {
        "type": "Breakfast",
        "recipes": [
            {
                "id": "9C682AD6-A94B-4545-8E60-DC9A5339CEFB",
                "name": "Fluffy flourless pancakes",
                "creator": "Jamie Oliver",
                "dateCreated": "2022-05-13T00:00:00+01:00",
                "serves": 2,
                "ingredients": [
                    {
                        "name": "porridge oats",
                        "quantity": 100,
                        "measurement": "g"
                    },
                   // ETC
                ],
                "method": [
                    {
                        "step": 1,
                        "text": "Put the oats, cottage cheese, 2 eggs and the baking powder in a blender and blitz until smooth with a tiny pinch of sea salt, adding a splash of water to loosen if it’s too thick."
                    },
                    // ETC
                ],
                "imageURL": "https://img.jamieoliver.com/jamieoliver/recipe-database/105840147.jpg"
            },
            {
                "id": "F1AFA8E4-5398-4ACB-AF9D-CC8D690B59D0",
                "name": "Chocolate and Raspberry Chia Bowl",
                "creator": "Joe Wicks",
                "dateCreated": "2022-04-13T00:00:00+01:00",
                "serves": 1,
                "ingredients": [
                    {
                        "name": "almond milk",
                        "quantity": 200,
                        "measurement": "ml"
                    },
                    // ETC
                ],
                "method": [
                    {
                        "step": 1,
                        "text": "Place the almond milk, cocoa powder, protein powder and maple syrup in a blender and blend until smooth."
                    },
                   // ETC
                ],
                "imageURL": "https://images.ctfassets.net/izjiv8mj8dix/7Duxf54vdOaxyUzWe1kUJv/73a1f452d2270c95c2ece74724e03a1d/Chocolate_and_Raspberry_Chia_Pot__vg_.jpg"
            }
        ]
    },
    {
        "type": "Lunch",
        "recipes": [
            {
                "id": "37D08017-F6C4-4CAB-99F3-6167A5107327",
                "name": "Pork Schnitzel with Garlic Mushrooms",
                "creator": "Joe Wicks",
                "dateCreated": "2022-05-13T00:00:00+01:00",
                "serves": 1,
                "ingredients": [
                    {
                        "name": "lean pork loin medallions",
                        "quantity": 160,
                        "measurement": "g"
                    },
                    // ETC
                ],
                "method": [
                    {
                        "step": 1,
                        "text": "Lay the pork between two sheets of clingfilm then bash with a rolling pin to flatten out into thin steaks. Season the pork with salt and pepper. Mix the breadcrumbs with the parsley and lemon, then coat the pork in the flour, shake off the excess then dip in the beaten egg. Finally dip in the breadcrumbs, making sure the pork is evenly coated."
                    },
                    // ETC
                ],
                "imageURL": "https://images.ctfassets.net/izjiv8mj8dix/3tkvDl4nYw8rdHYVdd4Q1b/41f00c5f4ea4c34600d7a7ce06e969c3/Pork_Schnitzel_with_Creamy_Garlic_Mushrooms.jpg"
            },
            {
                "id": "A4A3F244-1390-43D5-A850-D8A9AA76DD43",
                "name": "Orzo with Prawns and Cherry Tomatoes",
                "creator": "Joe Wicks",
                "dateCreated": "2022-05-13T00:00:00+01:00",
                "serves": 1,
                "ingredients": [
                    {
                        "name": "quark",
                        "quantity": 50,
                        "measurement": "g"
                    },
                    // ETC
                ],
                "method": [
                    {
                        "step": 1,
                        "text": "Put the quark and red pepper into a small food processor with a pinch of salt, blend until smooth then set aside."
                    },
                    // ETC
                ],
                "imageURL": "https://images.ctfassets.net/izjiv8mj8dix/2N4NkSYDKgBrIX1mO49CGR/c2583966908ba20290c8f1fc64a33bab/Orzo_with_Prawns_and_Cherry_Tomatoes.jpg"
            }
        ]
    },
    {
        "type": "Dinner",
        "recipes": [
            {
                "id": "8B19C463-EC8F-409C-92AD-B4CEC8362E67",
                "name": "Crab Courgette Spaghetti",
                "creator": "Mary Berry",
                "dateCreated": "2022-04-13T00:00:00+01:00",
                "serves": 14,
                "ingredients": [
                    {
                        "name": "spaghetti",
                        "quantity": 200,
                        "measurement": "g"
                    },
                    // ETC
                ],
                "method": [
                    {
                        "step": 1,
                        "text": "Cook the spaghetti in boiling salted water according to the packet instructions. Drain well."
                    },
                 // ETC
                ],
                "imageURL": "https://www.maryberry.co.uk/media/recipes/2021/12/14/154_inlineImage.jpg"
            },
            {
                "id": "D68E7D31-707C-45F3-932A-A72BA8180726",
                "name": "Crusted Salmon with Samphire & Preserved Lemon Sauce",
                "creator": "Mary Berry",
                "dateCreated": "2022-04-13T00:00:00+01:00",
                "serves": 4,
                "ingredients": [
                    {
                        "name": "semolina",
                        "quantity": 25,
                        "measurement": "g"
                    },
                   // ETC
                ],
                "method": [
                    {
                        "step": 1,
                        "text": "Preheat the oven to 200°C/180°C fan/Gas 6 and butter a baking sheet."
                    },
                   // ETC
                ],
                "imageURL": "https://www.maryberry.co.uk/media/recipes/2021/12/14/153_inlineImage.jpg"
            }
        ]
    }
]

Had to make shorten as to long for post but you can see the full version in the github under menu.json

Then you can make a Recipe struct of:

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

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

    let id: UUID
    let name: String
    let creator: String
    let dateCreated: Date
    let serves: Int
    let ingredients: [Ingredient]
    let method: [Method]
    let imageURL: URL
}

And also make a Menu struct of Menu

struct Menu: Codable {
    let type: String
    let recipes: [Recipe]
}

I have made not nested these unlike the Method and Ingredient beacuse will need direct access to Recipe

Make two enum

enum MenuSelection: String, CaseIterable {
    case all, vegan, breakfast, lunch, dinner
}

enum FilterSelection: String, CaseIterable {
    case all, latest 
}

NETWORK CALL

Add new file called URLSession-Codable

extension URLSession {
    func decode<T: Decodable>(
        _ type: T.Type = T.self,
        from url: URL,
        keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
        dataDecodingStrategy: JSONDecoder.DataDecodingStrategy = .deferredToData,
        dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate
    ) async throws  -> T {
        let (data, _) = try await data(from: url)

        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = keyDecodingStrategy
        decoder.dataDecodingStrategy = dataDecodingStrategy
        decoder.dateDecodingStrategy = dateDecodingStrategy

        let decoded = try decoder.decode(T.self, from: data)
        return decoded
    }
}

I have used a ViewModel file to put this in

@MainActor
final class ViewModel: ObservableObject {
    @Published var menus = [Menu]()

    @Published var selectedMenu = MenuSelection.all
    @Published var selectedFilter = FilterSelection.all

    func fetch() async {
        do  {
            let userURL = URL(string: "https://recipesstore.s3.eu-west-2.amazonaws.com/Menu.json")!
            // added `dateDecodingStrategy` as there is an ISO8601 date in JSON but not always required.
            async let userItems = try await URLSession.shared.decode([Menu].self, from: userURL, dateDecodingStrategy: .iso8601)
            menus = try await userItems
        } catch {
            print("Failed to fetch data!")
    }
}

Also add the "filtering" computed properties.

/// Filter the recipe depends on Menu Selected
private var menuType: [Recipe] {
    if selectedMenu == .all {
        return menus.flatMap { $0.recipes }
    } else {
        let selected = menus.filter { $0.type == selectedMenu.rawValue.capitalized }
        return selected.flatMap { $0.recipes }
    }
}

/// Filter the `menuType` if by date.
var filteredRecipes: [Recipe] {
    if selectedFilter == .all {
        return menuType
    } else {
        // change `value` depends on the last numbers of days required
        guard let hurdleDate = Calendar.current.date(byAdding: .day, value: -7, to: Date.now) else {
            fatalError("Unable to get day from date")
        }
        // Use `<=` if only want recipes before the date. (note: need to change the `FilterSelection` enum.
        return menuType.filter { $0.dateCreated >= hurdleDate }
    }
}

UI

Then in @main App file change to this. This will now only do one network call when app opens which is better for user data!

@main
struct CookbookApp: App {
    @StateObject var menus: ViewModel

    /// init required for @MainActor do push UI changes to main thread
    init() {
        self._menus = StateObject(wrappedValue: ViewModel())
    }

    var body: some Scene {
        WindowGroup {
            ContentView(vm: menus)
                .task { await menus.fetch() }
        }

    }
}

Then you can do in ContentView to use it

struct ContentView: View {
    @ObservedObject var vm: ViewModel

    var body: some View {
        NavigationView {
            List {
                Section {
                    Picker("Select menu", selection: $vm.selectedMenu) {
                        ForEach(MenuSelection.allCases, id: \.self) {
                            Text($0.rawValue.capitalized)
                        }
                    }

                    Picker("Select time", selection: $vm.selectedFilter) {
                        ForEach(FilterSelection.allCases, id: \.self) {
                            Text($0.rawValue.capitalized)
                        }
                    }
                    .pickerStyle(.segmented)
                }

                ForEach(vm.filteredRecipes) { recipe in
                    VStack(alignment: .leading) {
                        Text(recipe.name)
                        Text(recipe.creator)
                            .foregroundColor(.secondary)
                    }
                }
            }
            .navigationTitle("Menu")
        }
    }
}

   

Then you can add extra "menus" with without.

{
    "type": "Bread",
    "recipes": [
        {
            "id": "65A919DF-E2A3-4EC2-A74E-F5353FAB7396",
            "name": "White Loaf Bread",
            "creator": "Nigel Gee",
            "dateCreated": "2022-05-15T00:00:00+01:00",
            "serves": 5,
            "ingredients": [
                {
                    "name": "Water",
                    "quantity": 260,
                    "measurement": "g"
                },
                {
                    "name": "Olive oil",
                    "quantity": 1.5,
                    "measurement": "tbsp"
                },
                {
                    "name": "Salt",
                    "quantity": 1.5,
                    "measurement": "tsb"
                },
                {
                    "name": "Sugar",
                    "quantity": 1.5,
                    "measurement": "tbsp"
                },
                {
                    "name": "Dried Semi Skimmed milk",
                    "quantity": 1.5,
                    "measurement": "tbsp"
                },
                {
                    "name": "Strong White Flour",
                    "quantity": 400,
                    "measurement": "g"
                },
                {
                    "name": "Yeast",
                    "quantity": 1.5,
                    "measurement": "tsb"
                }
            ],
            "method": [
                {
                    "step": 1,
                    "text": "Dissolve the salt, sugar and dried milk in warm water."
                },
                {
                    "step": 2,
                    "text": "Add olive oil to container then put in flour and yeast on top"
                },
                {
                    "step": 3,
                    "text": "Inset container into bread maker and switch on."
                }
            ],
            "imageURL": "https://www.maryberry.co.uk/media/recipes/2021/12/14/153_inlineImage.jpg"
        }
    ]
}

this will show in all feed until user updates the app for menu selection

enum MenuSelection: String, CaseIterable {
    case all, vegan, breakfast, lunch, dinner, bread
}

PS I found a way getting UUIDs is to run this in playground then copy paste into JSON

for _ in 0..<10 {
    print(UUID())
}

1      

@roosterboy Here is what I tried but I keep getting this error saying :

  • Cannot infer contextual base in reference to member 'utf8'
  • Value of type '[Recipe]' has no member 'data'

My Json files are Dated as a string as such: "18/05/2022"

My Api Call:

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

    func fetchRecipes() async {

        guard let breakfastAPIURL = URL(string: URLData.breakfast.rawValue) else {
            fatalError("Missing URL") 
        }

          do {
              let (data, _) = try await URLSession.shared.data(from: breakfastAPIURL)
              let decoder = JSONDecoder()

              recipes = try decoder.decode([Recipe].self, from: data)

              //create a DateFormatter
              let formatter = DateFormatter()
              //and tell it what format we are expecting to receive
              formatter.dateFormat = "dd/MM/yyyy"

              //now use the DateFormatter as the dateDecodingStrategy
              decoder.dateDecodingStrategy = .formatted(formatter)

              let response = try decoder.decode([Recipe].self, from: recipes.data(using: .utf8)!)
              print(response)  //JSONDateTester(item: "thing", date: 2022-05-18 07:00:00 +0000)

              //recipes = recipes.filter { $0.dateAdded <= Date.now }

          } catch {
              print("Error decoding: ", error)
          }

Here is my struct if it helps:

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
    let dateAdded: Date
    let difficulty: String

    //Add new attributes to the CodingKeys
    enum CodingKeys: String, CodingKey {
        case id, name, creator, serves, ingredients, dateAdded, difficulty
        case methods = "method"
        case imageURL = "imageurl"
    }
}

   

You've already decoded your data once, so you don't need to do it a second time.

Add this property to your BreakfastAPI object:

//this creates a single formatter so we aren't always recreating it
//every time we call fetchRecipes()
static let ddMMyyyy: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "dd/MM/yyyy"
    return formatter
}()

Now change your do {} catch {} block to this:

do {
  let (data, _) = try await URLSession.shared.data(from: breakfastAPIURL)

  let decoder = JSONDecoder()
  //here we use the DateFormatter we set up earlier
  //it's a static property so we need to prefix with the class name
  decoder.dateDecodingStrategy = .formatted(BreakfastAPI.ddMMyyyy)

  recipes = try decoder.decode([Recipe].self, from: data)
  recipes = recipes.filter { $0.dateAdded <= Date.now }

} catch {
  print("Error decoding: ", error)
}

   

Hi,

This is what happens when I do this above:

Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  dataCorrupted(Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "dateAdded", intValue: nil)], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "dateAdded", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  dataCorrupted(Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "dateAdded", intValue: nil)], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  dataCorrupted(Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "dateAdded", intValue: nil)], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))
Error decoding:  dataCorrupted(Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "dateAdded", intValue: nil)], debugDescription: "Date string does not match format expected by formatter.", underlyingError: nil))
Error decoding:  keyNotFound(CodingKeys(stringValue: "dateAdded", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"dateAdded\", intValue: nil) (\"dateAdded\").", underlyingError: nil))

You can access the URL here if it helps.

Coding keys are the same as above.

   

As you seem to ignore my previous post on how to do it and want to go down this route. Here your Model

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

    private enum CodingKeys: String, CodingKey {
        case id, name, creator, dateAdded, difficulty, serves, ingredients
        case methods = "method"
        case categoryTag = "category tag"
        case imageURL = "imageurl"
    }
}

Using Paul's URLSession extension that I have giving you twice before. The class would look like this

@MainActor
class BreakfastAPI: ObservableObject {
    @Published var recipes = [Recipe]()

    func fetchRecipes() async {

        let formatter = DateFormatter()
        formatter.dateFormat = "dd/MM/yyyy"

        do  {
            let breakfastAPIURL = URL(string: URLData.breakfast.rawValue)!
            async let items = try await URLSession.shared.decode([Recipe].self, from: breakfastAPIURL, dateDecodingStrategy: .formatted(formatter))
            recipes = try await items
            recipes = recipes.filter { $0.dateAdded <= Date.now }
        } catch {
            print("Failed to fetch data!", error)
        }
    }
}

Now you can use it

struct ContentView: View {
    @StateObject var breakfastAPI: BreakfastAPI

    /// init required for @MainActor do push UI changes to main thread
    init() {
        self._breakfastAPI = StateObject(wrappedValue: BreakfastAPI())
    }

    var body: some View {
        List(breakfastAPI.recipes) { recipe in
            Text(recipe.name)
        }
        .task {
            await breakfastAPI.fetchRecipes()
        }
    }
}

   

The problem with your method is that I have a UI that isnt using Picker.

Here is the sample our my menu selector:

                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(alignment: .top, spacing: 0) {
                        ForEach(RecipeSelection.allCases) { menu in
                            NavigationLink {
                                RecipeList(menu: menu)
                            } label: {
                                VStack(alignment: .leading) {
                                    Image("navigation/\(menu.name)")
                                        .frame(width: 100)
                                        .cornerRadius(10)
                                        .foregroundColor(.gray)

                                    Text(menu.name)
                                        .fontWeight(.bold)

                                }
                                .padding(8)
                            }

                        }

                    }
                   .buttonStyle(PlainButtonStyle())
                }

Whatever menu they select they are then shown a list of recipes from that menu:

struct RecipeList: View {
    let menu: RecipeSelection

    @StateObject var api = RecipeAPI()

    var body: some View {

        List {
            //HeaderImage
            Image("HeaderImages/\(menu.name)")
                .resizable()
                .frame(height: 250)
                .aspectRatio(contentMode: .fill)
                .listRowBackground(Color.black)
                .listRowInsets(EdgeInsets(.zero))
                .padding(.bottom, 10)

            //View Discription and Title Header
            Text(menu.name)
                .font(.largeTitle)
                .fontWeight(.bold)
                .listRowSeparator(.hidden)
                .listRowBackground(Color.black)

            Text("Explore \(menu.name) recipes from creators you love.")
                .listRowBackground(Color.black)

            //List of Recipes
            ForEach(api.recipes) { recipe in

                NavigationLink(destination: RecipesLanding(recipe: recipe)){

                    HStack{
                        AsyncImage(url: URL(string: "\(recipe.imageURL)")) { image in
                            image
                                .resizable()
                                .cornerRadius(8)
                                .frame(width: 130, height: 81)
                                .clipped()
                                .aspectRatio(contentMode: .fit)
                        } placeholder: {
                            Rectangle()
                                .fill(Color.gray)
                                .frame(width: 130, height: 81)
                                .cornerRadius(8)
                        }

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

            }
            .listRowBackground(Color.black)
            .padding(5)

        }
        .frame( maxWidth: .infinity)
        .edgesIgnoringSafeArea(.all)
        .listStyle(GroupedListStyle())

        .task {
            await api.fetchRecipes(for: menu)
        }
        .listRowBackground(Color.black)

    }
}

Then whatever one they click they are shown that recipe:

struct RecipesLanding: View {
    var recipe: Recipe

    var body: some View {

        ScrollView{

            //Recipe Details
            VStack(alignment: .leading){
                Text(recipe.name)
                    .font(.title)
                    .fontWeight(.bold)
                HStack{
                    Text(recipe.creator)
                        .font(.subheadline)
                }
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()

            //Recipe Image
                AsyncImage(url: URL(string: "\(recipe.imageURL)")) { image in
                    image
                        .resizable()
                        .clipped()
                        .frame(height: 280)
                        .aspectRatio(contentMode: .fill)
                        .cornerRadius(8)
                } placeholder: {
                    Rectangle()
                        .frame(height: 280)

                }

            //Recipe Instructions and Ingredients
            VStack(alignment: .leading){
                HStack{
                    Text("Things you need 👇")
                        .font(.title)
                        .fontWeight(.bold)
                }
                .padding([.leading, .bottom])

                //Listing Ingredients
                ForEach(recipe.ingredients, id: \.name){ ingredient in
                    VStack {
                        HStack{
                            Text("\(Int(ingredient.quantity)) \(ingredient.measurement) \(ingredient.name) ")
                        }

                    }
                }
                .padding([.leading, .bottom, .trailing])
            }
            .frame(maxWidth: .infinity, alignment: .leading)

            VStack(alignment: .leading) {
                HStack{
                Text("Steps to make it happen!")
                    .font(.title)
                    .fontWeight(.bold)
                }
                .padding([.leading, .bottom])

                Spacer()
                //Listing Method
                ForEach(recipe.methods, id: \.step){method in
                    Divider()

                    Text("Step \(method.step)")
                        .font(.headline)

                    Text(method.text)
                }
                .padding([.leading, .bottom, .trailing])
            }

            Spacer()

        }
        .navigationBarTitle("", displayMode: .inline)

    }
}

So even though your method is good for the picker thing. I don't have that in my UI because it looks like this:

https://apps.apple.com/app/id1622900349

and one call gets passed to the next. All I wanted to do is filter that list of items that from the menu they select. My JSON doesnt have UUIDs and the UF Date format because It would be very long to add that to multiple recipes assuming I am adding 60 each month.

Can't I just somehow create a method that allows me to send a netowrk request to S3 for dateAdded = "16/05/2022" only rather than filterng it out on the app if it's simplier. Because that way it should only send back recipes with those dates. I can probs upload each recipe indivually as a JSON if it helps.

   

Hi Imran,

I see where the confusion is. You want to make a query on the NETWORK call! Both @roosterboy and I are give you solution on filtering the data after the network call. It easier to make filter/sort etc once you have the "data" in a Collection(an Array, Set or Dictionary). Which is why I suggested, in first post, changes the JSON (as I think you are making it and before you start add lots of recipes).

I only used iso8601 format for date as this is built into Swift JSONDecoder() and you do not have to use DateFormatter(), however if you want to use "dd/MM/yyyy" then you can add the formater before the do, try, catch block as did in second example or use @roosterboy solution.

I suggested using UUID instead of 1, 2, 3… (Ints) as someone who has made a JSON file with over 1550 items when using Ints, it was really difficult to keep track of the numbers and you can make mistakes easily and if you have the same number then List/ForEach might not work correctly. You are going to add 60 recipes a month to the JSON (720 a year wow)(Good luck keeping track of the ids then) then you add in next paragraph that you can make them individually!

Lastly I used a picker because of ease and not spend to much time on the UI, however still would work on NavigationLink or Button just as well.

Summary

Fetch the data then filter it. This why I suggested ONE JSON so to do ONE network call (plus AsyncImage) and all the filtering done on device. However if you want to have a separate JSON for EACH menu then you can but bear in mind that EVERY time the user taps a menu it could using Cellular Data!

Just a quick note you used

AsyncImage(url: URL(string: "\(recipe.imageURL)"))

You do not have to use URL(string: "\(recipe.imageURL). You can do

AsyncImage(url: recipe.imageURL)

PS your UI looks good.

2      

How do I add the UUIDs to the JSON file since I am typing them in manually. I don't mind filtering them afterwards because alot of GraphQL and GraphCMS etc are not great support for SwiftUI and Async Await. So prefer Apple API to filter it out if I can just need help in the parts of where do I add what:

So to simplify:

  1. What do I need to add to my xcode project to reformat the date and where specifically?
  2. How do I generate UUIDs to add to my JSONs?

How do I use this with environment object?

init() { self._breakfastAPI = StateObject(wrappedValue: BreakfastAPI()) }

   

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

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.