How to update #ObservedObject object while adding items to the class - swift

I have 2 sections (ingredients selection and ingredient added). In the ingredient selection, i get all items from a json file. The goal is whenever i chose an ingredient from the ingredient selection, the item should be added to the ingredient added section. However the actual result is that I am able to add all ingredients in AddedIngredients. However my #ObservedObject var ingredientAdded :AddedIngredients indicates no items, so nothing in the section.
What am I missing in my logic?
Is it because the #Published var ingredients = [Ingredient]() is from a struct which creates a new struct every time ? My understanding is that since AddedIngredients is a class, it should reference the same date unless I override it.
This my model:
import Foundation
struct Ingredient: Codable, Identifiable {
let id: String
let price: String
let ajoute: Bool
}
class AddedIngredients: ObservableObject {
#Published var ingredients = [Ingredient]()
}
The ingredient section that displays all ingredients:
struct SectionIngredientsSelection: View {
let ingredients: [Ingredient]
#StateObject var ajoute = AddedIngredients()
var body: some View {
Section(header: VStack {
Text("Ajouter vos ingredients")
}) {
ForEach(ingredients){ingredient in
HStack{
HStack{
Image("mais")
.resizable()
.frame(width: 20, height: 20)
Text(ingredient.id)
}
Spacer()
HStack{
Text(ingredient.price )
Button(action: {
ajoute.ingredients.append(ingredient)
print(ajoute.ingredients.count)
}){
Image(systemName: "plus")
.foregroundColor(Color(#colorLiteral(red: 0, green: 0.3257463574, blue: 0, alpha: 1)))
}
}
}
}
.listRowBackground(Color.white)
.listRowSeparator(.hidden)
}
}
}
Whenever I add an ingredient from the previous section, it should appear on this section.
struct SectionIngredientsSelected: View {
#ObservedObject var ingredientAdded :AddedIngredients
var body: some View {
Section(header: VStack {
HStack{
Text("Vos ingredients")
.textCase(nil)
.font(.headline)
.fontWeight(.bold)
.foregroundColor(.black)
Button(action: {print(ingredientAdded.ingredients.count)}, label: {Text("Add")})
}
}) {
ForEach(ingredientAdded.ingredients){ingredient in
HStack{
HStack{
Image("mais")
.resizable()
.frame(width: 20, height: 20)
Text(ingredient.id)
}
Spacer()
HStack{
Text(ingredient.price )
Button(action: {
}){
Image(systemName: "xmark.circle")
.foregroundColor(Color(#colorLiteral(red: 0, green: 0.3257463574, blue: 0, alpha: 1)))
}
}
}
}
.listRowBackground(Color.white)
.listRowSeparator(.hidden)
}
}
}
Main View
struct CreationView: View {
let ingredients: [Ingredient] = Bundle.main.decode("Ingredients.json")
var addedIngre: AddedIngredients
var body: some View {
ScrollView {
VStack{
VStack(alignment: .leading) {
Image("creation")
.resizable()
.scaledToFit()
Text("Slectionnez vos ingredients preferes").fontWeight(.light)
Divider()
.padding(.bottom)
}
}
}
List {
SectionIngredientsSelected(ingredientAdded: addedIngre)
SectionIngredientsSelection(ingredients: ingredients)
}
}
}

You are creating two different instances of AddedIngredients. These do not synchronize their content with each other. The simpelest solution would be to pull the AddedIngredients up into CreationView and pass it down to the sub views.
So change your CreationView to:
struct CreationView: View {
let ingredients: [Ingredient] = Bundle.main.decode("Ingredients.json")
#StateObject private var addedIngre: AddedIngredients = AddedIngredients()
and:
SectionIngredientsSelected(ingredientAdded: addedIngre)
SectionIngredientsSelection(ingredients: ingredients, ajoute: addedIngre)
and SectionIngredientsSelection to:
struct SectionIngredientsSelection: View {
let ingredients: [Ingredient]
#ObservedObject var ajoute: AddedIngredients
Remarks:
It is not perfectly clear for me how CreationView aquires addedIngre. From your code it seems it gets somehow injected. If this is the case and you need it upward the view hierachy, change the #StateObject to #ObservedObject. But make sure it is initialized with #StateObject.

Related

How could I make certain menu items appear depending on the navigation "bar" I press

I am building an app that will store multiple Nintendo consoles and their details (kinda like Mactracker but for Nintendo stuff).
I wanna store certain consoles in categories on the main menu but I'm not sure how I could do it (I'm pretty new to swiftui so sorry if that's a stupid question).
I already have categories set on MainMenu based on the category I've put in my consoles array. But I can't get the console menu (where all the consoles are) to store the consoles only of the category I tap on.
My consoles array has multiple consoles but I've put just one to save space.
Main Menu:
import SwiftUI
struct MainMenu: View {
var con: [ConsoleDetails] = ConsoleList.consoles
var body: some View {
NavigationView{
List(ConsoleList.categories.sorted(by: {$0.key > $1.key}), id:\.key){con in
NavigationLink(destination: ConsoleMenu(), label:{
Image(systemName: "folder.fill")
.scaledToFit()
.frame(height: 30)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(con.key)
.fontWeight(.semibold)
}
}).navigationTitle("app.name")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MainMenu()
}
}
ConsoleMenu:
import SwiftUI
struct ConsoleMenu: View {
var con: [ConsoleDetails] = ConsoleList.consoles
var body: some View {
NavigationView{
List(con, id:\.id){ cons in
NavigationLink(destination: ConsoleDetailView(con: cons), label:{
Image(cons.imgName)
.resizable()
.scaledToFit()
.frame(height: 50)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(cons.consoleName)
.fontWeight(.semibold)
}
}).navigationTitle("\(cons.category)")
}
}
}
}
struct ConsoleSection_Previews: PreviewProvider {
static var previews: some View {
ConsoleMenu()
.preferredColorScheme(.dark)
}
}
Consoles:
import Foundation
struct ConsoleDetails: Identifiable{
let id = UUID()
var imgName: String = ""
var consoleName: String = ""
var description: String = ""
var initialPrice: Double = 0.0
var ReleaseDate: String = ""
var Discontinuation: Int = 0
var category: String = ""
}
struct ConsoleList{
static let categories = Dictionary(grouping: consoles, by: {$0.category } )
static let consoles = [
ConsoleDetails(imgName: "FAMICOM",
consoleName: "Famicom",
description: "It was released in 1983 in Japan and due to it success it gave birth to the NES",
initialPrice: 179,
ReleaseDate: "Release Date: July 15, 1983",
Discontinuation: 2003,
category: "Home Consoles"),
//there's more consoles but I just put one to save space on here
Few modifications:
In main view use only categories and pass only useful consoles to console menu :
struct MainMenu: View {
// Use categories ordered by reversed alphabetical order
var categories = ConsoleList.categories.sorted(by: {$0.key > $1.key})
// var con: [ConsoleDetails] = ConsoleList.consoles
var body: some View {
NavigationView{
// Loop on categories
List(categories, id:\.key){category in
NavigationLink(destination: ConsoleMenu(con: category.value), label:{
Image(systemName: "folder.fill")
.scaledToFit()
.frame(height: 30)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(category.key)
.fontWeight(.semibold)
}
}).navigationTitle("app.name")
}
}
}
}
In Console menu use the consoles furnished by main menu
struct ConsoleMenu: View {
// Consoles are given by caller
var con: [ConsoleDetails] /* = ConsoleList.consoles */
var body: some View {
NavigationView{
List(con, id:\.id){ cons in
NavigationLink(destination: ConsoleDetailView(con: cons), label:{
Image(cons.imgName)
.resizable()
.scaledToFit()
.frame(height: 50)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(cons.consoleName)
.fontWeight(.semibold)
}
}).navigationTitle("\(cons.category)")
}
}
}
}

Get a specific id in a modal

I'm still learning on the job and my question may seem stupid.
I've got a list of movies and on the tap I want to show card of the selected movie.
So I've got my ResultsView
var results:[DiscoverResult]
#State private var resultsCount:Int = 0
#State private var isPresented:Bool = false
#EnvironmentObject private var genres:Genres
var body: some View {
ScrollView {
ForEach (results){ result in
Button(action: {
isPresented.toggle()
}, label: {
ZStack {
ZStack {
KFImage(URL (string: baseUrlForThumb + result.posterPath)).resizable().scaledToFill()
.frame( height: 150)
.mask(Rectangle().frame( height: 150))
Rectangle().foregroundColor(.clear) // Making rectangle transparent
.background(LinearGradient(gradient: Gradient(colors: [.clear, .clear, .black]), startPoint: .top, endPoint: .bottom))
}.frame( height: 150)
// Titre du film
VStack(alignment: .center) {
Spacer()
Text(result.title)
.fontWeight(.bold)
.foregroundColor(.white)
.multilineTextAlignment(.center)
// Genres du film
Text(genres.generateGenresList(genreIDS: result.genreIDS)).font(.caption).foregroundColor(.white).multilineTextAlignment(.center)
} .padding()
}.padding(.horizontal)
})
.sheet(isPresented: $isPresented, content: {
MovieView(isPresented: $isPresented, movieId: result.id)
})
.navigationTitle(result.title)
}
}
}
}
And my MovieView
import SwiftUI
struct MovieView: View {
#Binding var isPresented: Bool
var movieId:Int
var body: some View {
VStack {
Text(String(movieId))
.padding()
Button("Fermer") {
isPresented = false
}
}
}
}
But the movie card still the same even list element selected.
I think that the 'result.id' is overwrite at every loop but i don't know how to fix it.
Sorry for my english mistakes.
thank for your purpose.
Instead of using isPresented for .sheet you can use .sheet(item:, content:) and pass the whole result object
.sheet(item: $selecteditem( { result in
MovieView(item: result)
}
To make this work you need a new property (you can remove isPresented)
#State private var selectedItem: DiscoverResult?
and you need update your MovieView struct
struct MovieView: View {
let result: DiscoverResult
var body: some View {
//...
}
}
or pass only the movie id to your MovieView if you prefer that.

SwiftUI Textfield not actually making updates to data

I am trying to make an application where the user can see a list of foods and recipes and make changes etc. In the DetailView for each food, there is an EditView where the values can be changed. I am trying to use a double binding on the value by using #State to define the value and $food.name for example to make the changes happen.
When I click 'Done' and exit this view back to the DetailView, the changes are not made at all, leaving me confused?
Any help on this problem would be greatly appreciated, thank you :)
My EditView.swift:
import SwiftUI
struct EditView: View {
#State var food: Food
var body: some View {
List {
Section(header: Text("Name")) {
TextField("Name", text: $food.name)
}
Section(header: Text("Image")) {
TextField("Image", text: $food.image)
}
Section(header: Text("Desc")) {
TextField("Desc", text: $food.desc)
}
Section(header: Text("Story")) {
TextField("Story", text: $food.story)
}
}.listStyle(InsetGroupedListStyle())
}
}
struct EditView_Previews: PreviewProvider {
static var previews: some View {
EditView(food: cottonCandy)
}
}
My FoodDetail.swift:
import SwiftUI
struct FoodDetail: View {
#State var food: Food
#State private var isPresented = false
var body: some View {
VStack {
Image(food.image)
.resizable()
.frame(width: 300.0,height:300.0)
.aspectRatio(contentMode: .fill)
.shadow(radius: 6)
.padding(.bottom)
ScrollView {
VStack(alignment: .leading) {
Text(food.name)
.font(.largeTitle)
.fontWeight(.bold)
.padding(.leading)
.multilineTextAlignment(.leading)
Text(food.desc)
.italic()
.fontWeight(.ultraLight)
.padding(.horizontal)
.multilineTextAlignment(.leading)
Text(food.story)
.padding(.horizontal)
.padding(.top)
Text("Ingredients")
.bold()
.padding(.horizontal)
.padding(.vertical)
ForEach(food.ingredients, id: \.self) { ingredient in
Text(ingredient)
Divider()
}.padding(.horizontal)
Text("Recipe")
.bold()
.padding(.horizontal)
.padding(.vertical)
ForEach(food.recipe, id: \.self) { step in
Text(step)
Divider()
}.padding(.horizontal)
}.frame(maxWidth: .infinity)
}.frame(minWidth: 0,
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .center
)
}
.navigationBarItems(trailing: Button("Edit") {
isPresented = true
})
.fullScreenCover(isPresented: $isPresented) {
NavigationView {
EditView(food: food)
.navigationTitle(food.name)
.navigationBarItems(leading: Button("Cancel") {
isPresented = false
}, trailing: Button("Done") {
isPresented = false
})
}
}
}
}
struct FoodDetail_Previews: PreviewProvider {
static var previews: some View {
Group {
FoodDetail(food: cottonCandy)
}
}
}
Inside EditView, you want to have a Binding. Replace
#State var food: Food
with
#Binding var food: Food
... then, you'll need to pass it in:
.fullScreenCover(isPresented: $isPresented) {
NavigationView {
EditView(food: $food) /// make sure to have dollar sign
.navigationTitle(food.name)
.navigationBarItems(leading: Button("Cancel") {
isPresented = false
}, trailing: Button("Done") {
isPresented = false
})
}
}

Why My second view cannot jump back to the root view properly

My App currently has two pages, first page has a circle plus button which could lead us to a second page. Basically, I have a save button which after clicking it, we could get back to the rood page. I followed this link for going back to root view. I tried the most up voted code, his code works perfectly. I reduced his code to two scene (basically the same scenario as mine), which also works perfectly. But then I don't know why my own code, pasted below, doesn't work. Basically my way of handling going back to root view is the same as the one in the link.
//
// ContentView.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/22/20.
//
import SwiftUI
import UIKit
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
struct ContentView: View {
#EnvironmentObject private var fridge : Fridge
private var dbStartWith=0;
#State var pushed: Bool = false
#State private var selection = 1;
#State private var addFood = false;
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(1)
Text("random tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("profile")
}
.tag(0)
}.environmentObject(fridge)
}
}
struct FoodView: View{
var body: some View{
NavigationView{
Text("food destination view ");
}
}
}
struct AddFoodView: View{
#Binding var pushed : Bool
#EnvironmentObject private var fridgeView : Fridge
#State private var name = ""
#State private var count : Int = 1
#State private var category : String = "肉类";
#State var showCategory = false
#State var showCount = false
var someNumberProxy: Binding<String> {
Binding<String>(
get: { String(format: "%d", Int(self.count)) },
set: {
if let value = NumberFormatter().number(from: $0) {
self.count = value.intValue;
}
}
)
}
var body: some View{
ZStack{
NavigationView{
VStack{
Button (action: {
self.pushed = false ;
//let tempFood=Food(id: fridgeView.index!,name: name, count: count, category: category);
//fridgeView.addFood(food: tempFood);
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
ZStack{
if self.showCount{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCount=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $count,label: EmptyView()) {
ForEach(1..<100){ number in
Text("\(number)").tag("\(number)")
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
.offset(x:10,y:-10)
Spacer()
}
if self.showCategory{
let categoryArr = ["肉类","蔬菜类","饮料类","调味品类"]
ZStack{
Rectangle().fill(Color.gray)
.opacity(0.5)
VStack(){
Spacer(minLength: 0);
HStack{
Spacer()
Button(action: {
self.showCategory=false;
}, label: {
Text("Done")
}).frame(alignment: .trailing).offset(x:-15,y:15)
}
Picker(selection: $category,label: EmptyView()) {
ForEach(0..<categoryArr.count){ number in
Text(categoryArr[number]).tag(categoryArr[number])
}
}.labelsHidden()
} .frame(minWidth: 300, idealWidth: 300, maxWidth: 300, minHeight: 250, idealHeight: 100, maxHeight: 250, alignment: .top).fixedSize(horizontal: true, vertical: true)
.background(RoundedRectangle(cornerRadius: 27).fill(Color.white.opacity(1)))
.overlay(RoundedRectangle(cornerRadius: 27).stroke(Color.black, lineWidth: 1))
Spacer()
}.offset(x:10,y:20)
}
}
}.animation(.easeInOut)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you read my code carefully, there are some variables are missing referencing. That's because I pasted part of the code that relates to my issue.
Food Class
//
// Food.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/23/20.
//
import Foundation
class Food: Identifiable {
init(id:Int, name: String, count: Int, category: String){
self.id=id;
self.name=name;
self.count=count;
self.category=category;
}
var id: Int
var name: String
var count: Int
var category: String
}
Fridge class
//
// Fridge.swift
// refridgerator_app
//
// Created by Mingtao Sun on 12/27/20.
//
import Foundation
class Fridge: ObservableObject{
init(){
db=DBhelper();
let result = setIndex(database: db!);
self.index = result.1;
self.container=result.0;
}
var db:DBhelper?
var index : Int?
#Published var container : [Food]?;
func setIndex(database: DBhelper) -> ([Food],Int){
let foodList : [Food] = database.read();
var index=0;
for food in foodList{
index = max(food.id,index);
}
return (foodList,(index+1));
}
func updateindex(index: inout Int){
index=index+1;
}
func testExist(){
if let data = db {
print("hi")
}
else{
print("doesnt exist")
}
}
func addFood(food:Food){
self.db!.insert(id: self.index!, name: food.name, count:food.count, category: food.category);
self.container!.append(food);
}
}
Because you implemented a new NaviagtionView in AddFoodView. Simply remove this and it should work. Look at the link you provided. There is no NavigationView in the child.
Correct me if Im wrong but the core code parts here that produce this issue are as follows:
Here you start:
struct ContentView: View {
#State var pushed: Bool = false
// Deleted other vars
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(fridge.container!){
food in NavigationLink(destination: FoodView()) {
Text("HI")
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
// Here you navigate to the child view
NavigationLink(destination: AddFoodView(pushed: self.$pushed),isActive: self.$pushed) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
}.isDetailLink(false) )
}
Here you land and want to go back to root:
struct AddFoodView: View{
#Binding var pushed : Bool
// Deleted the other vars for better view
var body: some View{
ZStack{
NavigationView{ // <-- remove this
VStack{
Button (action: {
// here you'd like to go back
self.pushed = false;
} ){
Text("save").foregroundColor(Color.blue).font(.system(size: 18,design: .default)) }
}.navigationBarTitle("Three")
}
For the future:
I have the feeling you might have troubles with the navigation in general.
Actually it is really simple:
You implement one NavigationView at the "root" / start of your navigation.
From there on you only use NavigationLinks to go further down to child pages. No NavigationView needed anymore.

SwiftUI passing a saved value to parent view

I am fairly new to SwiftUI I am trying to figure out the best way to pass data from a child view to parent?
Thanks for the help I come from a Javascript (React) background so this is a little different for me
The way my child view works is the user clicks on an image to select that image.
I have #State binding that saves the imgUrl which is a String referring to name in Assets.
I am just not sure about the best way to pass that value to the parent component.
Here is the child view (imageSelector)
struct ImageSelector: View {
#State private var windowImgs = ["1", "2", "3","4","5","6","7","8","9","10","11","12","13", "14","15","16","17","18"]
#State private var imgPicked = ""
var body: some View{
ScrollView(Axis.Set.horizontal, showsIndicators: true){
HStack{
ForEach(0..<18){num in
Button(action:{
self.imgPicked = self.windowImgs[num]
print(self.imgPicked)
}){
Image("\(self.windowImgs[num])")
.renderingMode(.original)
.resizable()
.cornerRadius(4)
.frame(width: 100, height: 100)
}
}
}
}
}
}
Here is the parent view (AddCounterForm)
struct AddCounterForm: View {
#Environment(\.presentationMode) var presentationMode
#State private var pickedImg: String = "defaultImg"
#State private var price: String = "0.0"
#State private var qty: String = "0"
var body: some View {
VStack (spacing: 40){
HStack {
Button("Cancel"){
self.presentationMode.wrappedValue.dismiss()
}
.foregroundColor(.red)
Spacer()
Button("Save"){
}
}
HStack {
VStack (spacing: 20){
TextField("Window type", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
TextField("Window location", text: /*#START_MENU_TOKEN#*//*#PLACEHOLDER=Value#*/.constant("")/*#END_MENU_TOKEN#*/)
}
.textFieldStyle(RoundedBorderTextFieldStyle())
Image(pickedImg)
.resizable()
.cornerRadius(4)
.frame(width: 90, height: 90)
.padding(.leading)
}
HStack {
Text("Price")
TextField("", text:$price)
.frame(width: 70)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
Spacer()
Text("Qty")
TextField("", text:$qty)
.frame(width: 70)
.textFieldStyle(RoundedBorderTextFieldStyle())
.keyboardType(.numberPad)
}
VStack {
Text("Select an image")
.foregroundColor(.blue)
ImageSelector()
.padding(.bottom)
Button("Use your own image"){
//method
}
.frame(width: 180, height: 40)
.background(Color.blue)
.clipShape(Capsule())
.foregroundColor(.white)
.padding(.top)
}
}
.padding()
}
}
Solution for preview thanks for the help from #Asperi & #neverwinterMoon
struct ImageSelector_Previews: PreviewProvider {
static var previews: some View {
PreviewWrapper()
}
}
struct PreviewWrapper: View {
#State(initialValue: "") var imgPicked: String
var body: some View {
ImageSelector(imgPicked: $imgPicked)
}
}
In this case Binding is most appropriate
struct ImageSelector: View {
#Binding var imgPicked: String
and use
ImageSelector(imgPicked: $pickedImg)
.padding(.bottom)