|
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 Int s 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())
}
|
|
@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… (Int s) as someone who has made a JSON file with over 1550 items when using Int s, 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 id s 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.
|
|
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:
- What do I need to add to my xcode project to reformat the date and where specifically?
- How do I generate UUIDs to add to my JSONs?
How do I use this with environment object?
init() {
self._breakfastAPI = StateObject(wrappedValue: BreakfastAPI())
}
|