SwiftUI Getting Unique Values from a Struct (from a JSON) - swift

I'm new to Swift and stuck on what could be a simple problem...
Background:
I'm trying to build a recipe app that has different recipes for each day of the week (7 days)
A json contains all the data in the hierarchy: Days -> have multiple Recipes -> have multiple Ingredients, where the ingredients have the following format:
"ingredients": [
{
"item": "cereal",
"amount": 1,
"units": "cup(s)"
},
{
"item": "milk",
"amount": 0.2,
"units": "L"
}
]
Desired Outcome:
What I'm trying to achieve is a shopping list that shows the unique list of all items that are needed for ALL recipes
How far I got so far:
The data is pulled from the json in this format which works:
struct Day: Hashable, Codable, Identifiable {
//Breakfast
var breakfastData: mealResponse
//Dinner
var dinnerData: mealResponse
struct mealResponse: Hashable, Codable {
struct IngredientResponse: Hashable, Codable {
var item: String
var amount: Double
var units: String
}
}
}
Then I manage to get the full list of ingredients like this:
ForEach(userData.days, id: \.self) { day in
ForEach(day.breakfastData.ingredients, id: \.self) {ingredient in
Text(ingredient.item)
}
}
But this is not a unique list (it just goes through every day and lists ALL ingredients for the breakfast recipes in this case...)
Would be so grateful for some help on this - have literally spent weeks trying all sorts of things...
Bonus:
The next step is then to show the sum of the "quantities" next to this unique list so the user knows how much of each ingredient s/he needs to make all the recipes in the json

To get an array of all ingredients you can use reduce and work with a Set to only add unique ingredients
let allIngredients = Array(days.reduce(into: Set<String>()) {
$0.formUnion($1.breakfastData.ingredients.map(\.item))
$0.formUnion($1.dinnerData.ingredients.map(\.item))
})
Regarding the "bonus", first of all you should only ask one question at a time but secondly let me just say that this might be much harder than expected since different recipes might use different units for the same ingredients so you will have to use a common unit between them before you can add them together. Also in your example you have a cup of cereal but that is something you buy a box of so there is another problem

Related

Swift - Is there a way to remove data from API that doesn't contain a certain key?

I am using Tasty recipes/list API from (https://rapidapi.com/apidojo/api/tasty/).
Unfortunately, this API is a mess (but it is the only one I could find that allows you to search for recipes by keyword and return a list of ingredients and instructions. As well as have a response time of under 8 seconds).
When I do a search it will either return a list of recipes or an item with a list of recipes. I am just trying to retrieve a list of recipes (and remove the ones that contain a list). I've found that the data I want contains a list of 50 items while the ones I do not want contain 28 items.
So would there be a way to check how many items that piece of data has and remove it from the list of data I am retrieving?
I have the following data structure:
struct Response: Decodable, Encodable {
let results: [Data]
}
struct Data: Decodable, Hashable, Encodable {
let name: String
let thumbnail_url: String
let sections: [Section]
let instructions: [Instruction]
let num_servings: Int
let prep_time_minutes: Int?
let cook_time_minutes: Int?
let total_time_minutes: Int?
}
struct Section: Decodable, Hashable, Encodable {
let components: [Ingredient]
}
struct Ingredient: Decodable, Hashable, Encodable {
let raw_text: String
}
struct Instruction: Decodable, Hashable, Encodable {
let display_text: String
}
I can provide more info/code upon request. Thank you I appreciate it!
Here is a picture of the data it returns (from the RapidAPI link above)

SwiftUI Country Picker - Show Country Name but store Country ID

UPDATE: I updated the code to my previous semi-working solution because there were multiple answers, but none answered the question the way I need it to work.
Also, note that I need United States at the top of the picker, even if it appears again in the alphabetical country listing.
I am trying to create a picker that displays a country name, and depending on what country is selected, stores the corresponding country id. This way the user sees the name of the country but I can pass only the country id into my database.
The code I have so far shows the list of country names, and stores that country name in the selectedCountry variable. It also updates the text element in the HStack properly.
The only thing that is not working is storing the corresponding countryId.
I am using SwiftUI with the latest Swift 5 and XCode 13.1.
Here's what I've got so far:
import SwiftUI
struct Country: View {
#State private var selectedCountry = ""
#State private var selectedCountryId = ""
let countryId = Locale.isoRegionCodes
let countryArray = Locale.isoRegionCodes.compactMap { Locale.current.localizedString(forRegionCode: $0) }
var body: some view {
HStack {
Text("Country:")
.font(.system(size: 17))
Spacer()
Text("")
if selectedCountry != "" {
Text("\(selectedCountry)")
.font(.system(size: 17))
.foregroundColor(Color("WhiteText"))
} else {
Text("Select Country")
.font(.system(size: 17))
.foregroundColor(Color("GrayText"))
}
} // End HStack
.onTapGesture {
self.showsCountryPicker.toggle()
}
Picker("Country", selection: $selectedCountry) {
ForEach(countryArray, id: \.self) {
Text($0)
}
}
.pickerStyle(WheelPickerStyle())
.padding()
.labelsHidden()
}
}
}
I'm sure it's completely the wrong way to do this, so don't worry so much about correcting my code. I'd really just love to know how to do this, because I'll also need to implement the same thing when it comes to selecting a US State (i.e. show the full name of the State but store the abbreviation).
Also, there is much more to the body view, but I've stripped down the code here just to show this specific issue.
Thanks in advance!
The Picker documentation says to use the tag modifier on each Text to control what the Picker stores in its selection.
There's no reason to store an array of country names if you just want to store the selected country code. And you should use SwiftUI's Environment to get the current Locale, so that your view will be redrawn if the user changes her locale.
import SwiftUI
import PlaygroundSupport
struct CountryPicker: View {
#Binding var countryId: String
#Environment(\.locale) var locale
var body: some View {
Picker("", selection: $countryId) {
ForEach(Locale.isoRegionCodes, id: \.self) { iso in
Text(locale.localizedString(forRegionCode: iso)!)
.tag(iso)
}
}
}
}
struct Test: View {
#State var countryId: String = ""
var body: some View {
VStack {
CountryPicker(countryId: $countryId)
Text("You picked \(countryId).")
}
.padding()
}
}
PlaygroundPage.current.setLiveView(Test())
I appreciate all the assistance, but I got it all working the way I needed it to. For this I am storing only the Country ID, which is all I need, but translating that ID into the country name for the text element in the HStack.
Here's the answer:
import SwiftUI
// Struct to store the country name and ID
fileprivate struct Country {
var id: String
var name: String
}
// Function to put United States at the top of the list
fileprivate func getLocales() -> [Country] {
let locales = Locale.isoRegionCodes
.filter { $0 != "United States"}
.compactMap { Country(id: $0, name: Locale.current.localizedString(forRegionCode: $0) ?? $0)}
return [Country(id: "US", name: Locale.current.localizedString(forRegionCode: "US") ?? "United States")] + locales
}
struct Test: view {
// selectedCountry stores the countryID (i.e. US)
#State private var selectedCountry: String = ""
// Main UI
var body: some View {
HStack {
Text("Country:")
.font(.system(size: 17))
Spacer()
Text("")
if selectedCountry != "" {
Text(Locale.current.localizedString(forRegionCode: selectedCountry) ?? selectedCountry)
.font(.system(size: 17))
.foregroundColor(Color("WhiteText"))
} else {
Text("Select Country")
.font(.system(size: 17))
.foregroundColor(Color("GrayText"))
}
} // End HStack
Picker("Country", selection: $selectedCountry) {
ForEach(getLocales(), id: \.id) { country in
Text(country.name).tag(country.id)
}
}
}
}
I would just do something simple like this:
struct Country: Identifiable, Hashable {
let id: String
let name: String
}
struct CountryView: View {
let countries = Locale.isoRegionCodes.compactMap{
Country(id: $0, name: Locale.current.localizedString(forRegionCode: $0)!) }
#State var selectedCountry: Country?
var body: some View {
Picker("Country", selection: $selectedCountry) {
ForEach(countries) {
Text($0.name).tag(Optional($0))
}
}.pickerStyle(.wheel)
.onChange(of: selectedCountry) { selected in
if let cntry = selected {
print("--> store country id: \(cntry.id)")
}
}
}
}
If you want to sort the countries, use this:
ForEach(countries.sorted(by: { $0.name < $1.name })) { ... }
This my friend is where dictionaries come in handy. A dictionary has two parts Key and Value or in swift terms ["Key":"Value"] There are three things to note about a dictionary.
#1, all key-value-pairs MUST be the same type, for example, [32:"String", 33: "String"] Which is important to remember.
#2, it does NOT guarantee order.
#3, It can only contain unique keys.
How does this apply to your problem? Well, it has to do with the type of data that you have. Currently you have 2 separate arrays, one with the Country, and one with the Codes. You can combine them into the dictionary and ensure that they are always together, referencing the value from the key, or searching for the value, to get a key or multiple keys. That previous sentence is important to pay attention to, though not for your case, you're guaranteed to only get one value for one key, but you can get multiple keys from one value. In your case you have a unique country, and unique ID.
var countries = ["USA": 9999,
"UK": 9998,
"Canada": 9997] // Etc..
Getting a value from a dictionary is even easier, but works similar to an array. You sub-script it. For example:
var canadaID= countries["Canada"]
Now it gets trickier getting a key from a value because you have to iterate over the whole dictionary to grab it. It's also possible that there are duplicate values, meaning you could technically get back an array of "Keys". In this example, I grabbed only the first found value. Again, remember that the order is not guaranteed and if you have multiple of the same value you may get the incorrect key.
var countryID = 9998
if let key = countries.first(where: { $0.value == someValue })?.key {
print(key)
}
From here it becomes trivial to store it.
func storeCountryIDFromKey(country: String) {
let countryId = countries[country]
// Store your ID.
}
What if my order is important??!??
This could be important for your case as you might want to display the countries in alphabetical order. To do that simply map the keys to an array and sort, as is tradition.
let keys: [String] = countries.map{String($0.key) }
Solution
This is a working solution. I'll leave it up to you to sort the arrays and link the data where you need it to go. You could use onChange(...) or even a Button(..) to handle the update, however your ID is the selectedCountry in this example.
struct FirstView: View {
#State var countries = ["US": 1,
"UK": 2,
"Canada": 4]
#State var selectedCountry = 1
var body: some View {
VStack {
Picker("Country", selection: $selectedCountry) {
let countriesArray = countries.keys.map({$0})
ForEach(countriesArray, id: \.self ) { country in
Text(country).tag(countries[country]!)
}
}.pickerStyle(.wheel)
Text("\(selectedCountry)")
}
}
}
Additional Reading
There is a concept in programming called Big-O notation typically expressed as O(n) or pronounced O-of-N. Which is the way that we describe time and space complexities. It's a great skill to learn if you want to become a great developer as it has to do with Data Structures and Algorithms. To make more sense of this, as it applies to your question, having two separate arrays to loop over vs one dictionary effectively takes 2x as long to accomplish with the double arrays. Furthermore it doubles the space complexity. Combining both into one Dictionary reduces your performance overhead by 1/2 which is a huge performance gain. With a small data-set such as countries, which there are a finite amount, it doesn't really matter; However, if you start working with massive datasets then suddenly 1/2 faster is a substantial performance boost.
Without digging too much into it, and to simply get your wheels spinning, every time you make a variable, or the compiler does that for you, that increases space complexity. Every time you run a line of code, or loop over a line of code, that increases the time complexity. Always, and I mean always, try your best to reduce that overhead. It'll force you to think outside the box and in turn, you'll learn better practices.
For creating and sorting the array of countries, this is my suggestion
// create a Country struct
struct Country:Equatable{
let code:String
let name:String
}
/** creating the array by first getting the
the codes and then sorting it, bubbling US to the top
[Sorted by][1]
**/
let countries = Locale.isoRegionCodes.compactMap
{ Country(code:$0,name:Locale.current.localizedString(forRegionCode: $0) ?? "")
}.sorted{
switch ($0,$1){
case ($0,$1) where $0.code == "US":
return true
case ($0,$1) where $1.code == "US":
return false
default:
return $0.name<$1.name
}
}
You can now store your selection and get the code and name or whatever you wish to by changing the struct as per your needs

SwiftUI Initialzier requires String conform to Identifiable

I am attempting writing some SwiftUI code for the very first time and am running into an issue while trying to make a simple table view. In this code teams is an array of String, but the List is giving me the following error: Initializer 'init(_:rowContent:)' requires that 'String' conform to 'Identifiable'
var body: some View {
let teams = getTeams()
Text("Hello there")
List(teams) { team in
}
}
Anyone have an idea what it means?
The argument of List is required to be unique, that's the purpose of the Identifiable protocol
You can do
List(teams, id: \.self) { team in
but then you are responsible to ensure that the strings are unique. If not you will get unexpected behavior.
Better is to use a custom struct with an unique id conforming to Identifiable
The Apple preferred way to do this is to add a String extension:
extension String: Identifiable {
public typealias ID = Int
public var id: Int {
return hash
}
}
See this code sample for reference:
https://github.com/apple/cloudkit-sample-queries/blob/main/Queries/ContentView.swift
I'm assuming that getTeams() returns a [String] (a.k.a. an Array<String>).
The issue, as the error suggests, are the strings aren't identifiable. But not just in the "they don't conform to the Identifiable protocol" sense, but in the broader sense: if two strings are "foo", one is indistinguishable from the other; they're identical.
This is unacceptable for a List view. Imagine you had a list that contains "foo" entries, and the users dragged to rearrange one of them. How would List be able to tell them apart, to know which of the two should move?
To get around this, you need to use an alternate source of identity to allow the List to distinguish all of its entries.
You should watch the "Demystify SwiftUI" talk from this year's WWDC. It goes into lots of detail on exactly this topic.
If your trying to go over a 'List' of items of type 'Struct'
Change the Struct to use 'Identifiable':
my Struct:
struct Post : Identifiable{
let id = UUID()
let title: String
}
my List :
let posts = [
Post( title: "hellow"),
Post( title: "I am"),
Post( title: "doing"),
Post( title: "nothing")
]
then you can go over your List:
List(posts){post in
Text(post.title
}

SwiftUI - using all but one values of Enum for different Pickers

In my app, I need to use a Picker in two different places. In one place, it's to set a property to one of three values: A, B, or C. But, in another place, I want to use the picker to filter a array of items to show either all items (no filter) or only the A, B, or C items.
So, my first attempt was to create an enum like so:
enum Category {
case all
case A
case B
case C
}
Using this, I can filter my list successfully. But, when I try to create a new item in the array, I don't want all to be an option. The user should only see a picker with A, B, and C.
I could try:
enum Category {
case A
case B
case C
}
but then how do I add the all option for the filter picker?
(To address some comments below, here are some more details on the specific thing I'm try to accomplish...
A tournament can be a men's, women's, or mixed tournament. On the screen where I list the tournaments, I want to be able to show all tournaments or just the men's, just the women's or just the mixed tournaments. So, I have a picker that spans the width of the iPhone. Looks and works great.
Obviously, when adding a new item to the list, I have to specify its category, but not "all". Also a picker.
So, in one case, I need three values, in the other case, I need the same three values with "All" added at the beginning.)
You should define your enum without the all case, because all is not a valid Category for an item. (This is a programming guideline known as “make illegal states unrepresentable”.)
enum Category: Hashable, CaseIterable {
case a
case b
case c
}
With that definition, the Picker for setting an item's property can look like this:
Picker("Category", selection: $category) {
ForEach(Category.allCases, id: \.self) { category in
Text(verbatim: "\(category)")
.tag(category)
}
}
Then you should recognize that your filter is optional. You can filter by item category, or you can perform no filtering. So your filter property should be declared optional:
#Binding var categoryFilter: Category?
The Picker for setting the filter then needs to be careful to use optional tags:
Picker("Category Filter", selection: $categoryFilter) {
Text("None")
.tag(Category?.none)
ForEach(Category.allCases, id: \.self) { category in
Text(verbatim: "\(category)")
.tag(Category?.some(category))
}
}
You can create enum in this way
enum Category {
case all
case A
case B
case C
static let categories: [Category] = [.A, .B, .C]
}
And then use the categories array when you need to choose among three.
You can make your enum CaseIterable and then you can use the allCases property when you want to display all enum values, and use a filter when you only want certain cases displayed.
NOTE: In my sample code, you'll need to replace selection: .constant(Category.a) with an #Binding
struct Test {
enum Category: String, CaseIterable {
case all = "All"
case a = "A"
case b = "B"
case c = "C"
}
struct TestView: View {
var body: some View {
VStack {
Picker("Picker Title", selection: .constant(Category.a)) {
ForEach(Category.allCases, id: \.self) { category in
Text(category.rawValue).tag(category)
}
}
Picker("Picker Title", selection: .constant(Category.a)) {
ForEach(Category.allCases.filter({ $0 != .all }), id: \.self) { category in
Text(category.rawValue).tag(category)
}
}
}
}
}
}
To answer your question if I understood you correctly.
Modify your enum to implement the CaseIterable take a look at it below.
enum Category:CaseIterable{
case all
case A
case B
case C
}
Filter out where your category matches the all case. Example below.
let preferredcategory = Category.allCases.filter{ return $0 != Category.all}
To test our result see the code below.
print(preferredcategory.count)
for catname in preferredcategory {
print("Hello, \(catname)!")
}
You can read more on this respective pages.
https://developer.apple.com/documentation/swift/caseiterable
https://developer.apple.com/documentation/swift/caseiterable/2994869-allcases
https://www.hackingwithswift.com/example-code/language/how-to-list-all-cases-in-an-enum-using-caseiterable
Thanks.
So you can use the preferredCategory variable to show the user your category for the one you do not want all to show. you can also use the same filter to show only the part of the enum that has all case as well.
Also, the good thing is you get to keep your enum and reuse it everywhere in your application without having to hardcode values. if you want to show only All you just have to filter out the remaining case.
Some people may want to downvote without a valid reasons but if you believe the approach is wrong feel free to edit or leave a comment on how to improve the solution.

Understanding list identifiers in SwiftUI

Apple tutorial:
Lists work with identifiable data. You can make your data identifiable in one of two ways: by passing along with your data a key path to a property that uniquely identifies each element, or by making your data type conform to the Identifiable protocol.
I am curious what the implications are for lists that show items by design that are semantically "equal", creating duplicate rows that should behave in the same manner (i.e. both be deleted on removal by id). For instance:
List(["a", "b", "b", "c", "c"], id: \.self) { str in
Text(str)
}
I think I saw some sources saying that each row must be uniquely identified. Is that really true or should it be identifi-able?
The code above doesn't seem to crash and works fine -- is it actually fine?
The code above doesn't seem to crash and works fine -- is it actually
fine?
Yes! Definitely, List and ForEach needs their item's be in some way identifiable. What does it means for us? That means we should provide unique items to List or ForEach! But you would say wait a moment I did not make my array or items unique, and instead of that I made even duplicate items as well, So why it does worked?
Well I would say you absolutely made all your items unique, in this way that only 1 of them exist not 2 or . . ., you would say how? the answer is in id: \.self when you use id: \.self term you are literally saying and conforming to Integer Indexing ID system from SwiftUI, with using id: \.self SwiftUI understand that you are not going show a way for SwiftUI to understand which item is which, then it start to work by itself, and given an undercover id to your items.
So you can even delete the part of: id: \.self from your code, if you can show a way to SwiftUI to make your items Identifiable, the way that SwiftUI works in this case is awesome, when you remove id: \.self it starts analysing the type of item in your List/ForEach, then automatically goes to search identifiable protocol in that type, So that means your CustomType also should conform identifiable.
Let me gave you an example that we are going delete id: \.self and even making more duplicate and also deleting items, all would be identifiable.
import SwiftUI
struct ContentView: View {
#State private var array: Array<String> = ["a", "a", "a", "b", "b", "b", "c", "c", "c"]
var body: some View {
List(array) { item in
Text(item)
}
Button("remove last item") {
if array.count > 0 { array.remove(at: array.count - 1) }
}
.foregroundColor(Color.red)
.font(Font.body.weight(Font.Weight.bold))
}
}
extension String: Identifiable {
public var id: UUID {
get {
return UUID()
}
}
}