How could I correctly implement favourites in SwiftUI? - swift

I am building an app that will store multiple Nintendo consoles and their details (kinda like Mactracker but for Nintendo stuff).
I wanna store consoles that the user chooses in a favourites category on the main menu but I can't implement it correctly.
I have the main menu which shows the different categories as well as the favourite category which is duplicated for each category:
Each favourites "button" takes me to the favourites of only a specific category (the category beneath it) and I would like that all of them are in only one tab and not in multiple tabs like they are so far.
The favourites category is a bool defined in the Console List which will be listed below.
Thanks for the help and sorry if its a stupid question, I'm quite new to programming.
Main Menu:
struct MainMenu: View {
// Use categories ordered by alphabetical order
var categories = ConsoleList.categories.sorted(by: {$0.key < $1.key})
var body: some View {
// Loop on categories
NavigationView{
List(categories, id:\.key){category in
// The NavigationLink that takes me to the favorites view
NavigationLink(destination: Favorites(con: category.value)){
Image(systemName: "heart")
Text("Favorites")
.fontWeight(.semibold)
}
NavigationLink(destination: ConsoleMenu(con: category.value), label:{
Image(systemName: "folder")
.foregroundColor(.red)
.scaledToFit()
.frame(height: 30)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(category.key)
.fontWeight(.semibold)
}
})
}
.navigationTitle("")
}
}
}
Favourites menu:
struct Favorites: View {
var con: [ConsoleDetails]
var body: some View {
List(con){ cons in
if cons.favorites {
NavigationLink(destination: ConsoleDetailView(con: cons), label:{
Image(cons.imgName)
.resizable()
.scaledToFit()
.frame(height: 50)
.cornerRadius(4)
.padding(.vertical, 4)
.navigationTitle("\(cons.category)")
VStack (alignment: .leading){
if cons.category == "Game & Watch" {
Text(cons.consoleName)
.fontWeight(.semibold)
Text(cons.mostSoldGame)
.font(.subheadline)
}else{
Text(cons.consoleName)
.fontWeight(.semibold)
}
}
}).navigationTitle("Favorites")
}
}
}
}
struct favorites_Previews: PreviewProvider {
static var previews: some View {
Favorites(con: ConsoleList.consoles)
}
}
The consoles list:
struct ConsoleDetails: Identifiable{
let id = UUID()
var imgName: String = ""
var consoleName: String = ""
var mostSoldGame: String = ""
var initialPrice: String = ""
var ReleaseDate: String = ""
var Discontinuation: String = ""
var category: String = ""
var estimatedPricedToday: String = ""
var cables: String = ""
var favorites: Bool
}
struct ConsoleList{
//The consoles list has more consoles usually but I'll only put one to save space
static var categories = Dictionary(grouping: consoles, by: {$0.category } )
static var favs = Dictionary(grouping: consoles, by: {$0.favorites} )
static var consoles = [
//Current Consoles
ConsoleDetails(imgName: "NS_OG",
consoleName: "Nintendo Switch",
mostSoldGame: "Mario Kart 8 Deluxe",
initialPrice: "299.99",
ReleaseDate: "Mar 3, 2017",
Discontinuation: "Still Available",
category: "Current Consoles",
estimatedPricedToday: "$200-250 used",
cables: "HDMI, USB Type-C",
favorites: true),
Edit: I have added what was proposed but I get an error in the nav link. See below:
List{
Section {
// The NavigationLink that takes me to the favorites view
NavigationLink {
Favorites() // Right Here: Missing argument for parameter 'con' in call
} label: {
Image(systemName: "heart")
Text("Favorites")
.fontWeight(.semibold)
}
}
Section {
ForEach(categories, id: \.key){ category in
NavigationLink(destination: ConsoleMenu(con: category.value), label:{
Image(systemName: "folder")
.foregroundColor(.red)
.scaledToFit()
.frame(height: 30)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(category.key)
.fontWeight(.semibold)
}
})
}
}
}

Instead of having the list loop over the categories, you could put a ForEach in the List.
Then, you could have one favorites link, and put it in a different section to differentiate it.
List {
Section {
NavigationLink {
Favorites(con: /* (1) */ ConsoleList.consoles)
} label: { /* ... */ }
}
Section {
ForEach(categories, id: \.key) { category in
// ...
}
}
}
(1) You will also need to pass in a list of all the consoles to show.

Related

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

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.

SwiftUI how to prevent view from automatically exiting when parent views list filters the item out

Hi I am fairly new to SwiftUI and I am building an application that has a list of items that, when clicked, show details about that item. There is an option to favorite these items and with a segmented picker at the top it filters the items to 'All items' or 'Favorites'. The problem is when the picker is on favorites, the user selects an item, and unfavorites the item in it's detail view, the user is automatically kicked out of it's detail view. Is there a simple way to prevent this?
Here is the code in the ContentView
import SwiftUI
struct ContentView: View {
#EnvironmentObject var vm : ViewModel
#State private var BiasStruct: BiasData = BiasData.allBias
#State var searchText = ""
#State var filterSearchText = ""
#State var selected = 1
var body: some View {
NavigationView{
VStack{
Picker("Hello", selection: $selected, content: {
Text("All Biases").tag(1)
Text("Favorites").tag(2)
})
.onChange(of: selected) { tag in //When selected is changed, sort the Favs list and reset search text
vm.sortFavs()
searchText = ""
filterSearchText = ""
}
.pickerStyle(SegmentedPickerStyle())
if (selected == 1){
List{
ForEach(searchText == "" ? BiasStruct.biases : BiasStruct.biases.filter({
$0.name.lowercased().contains(searchText.lowercased())
}), id: \.id){ entry in
HStack{
NavigationLink(destination: DetailView( thisBiase: $BiasStruct.biases[entry.id-1]), label: {
Text("\(entry.name)")
Image(systemName: vm.contains(entry) ? "heart.fill" : "heart")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
.onTapGesture {
vm.toggleFav(item: entry)
}
})
}
}
}
.searchable(text: $searchText)
.navigationTitle("Biases")
.cornerRadius(15)
}else if (selected == 2){
List{
ForEach(filterSearchText == "" ? vm.filteredItems :vm.filteredItems.filter({
$0.name.lowercased().contains(filterSearchText.lowercased())
}), id: \.id){ entry in
HStack{
NavigationLink(destination: DetailView( thisBiase: $BiasStruct.biases[entry.id-1]), label: {
Text("\(entry.name)")
Image(systemName: vm.contains(entry) ? "heart.fill" : "heart")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
.onTapGesture {
vm.toggleFav(item: entry)
}
})
}
}
}
.searchable(text: $filterSearchText)
.navigationTitle("Favorites")
.cornerRadius(15)
}
}
}
.padding(8)
}
}
This is the code in the DetailView that toggles the favorite
import SwiftUI
struct DetailView: View {
var body: some View {
Image(systemName: vm.contains(thisBiase) ? "heart.fill" : "heart")
.frame(alignment: .trailing)
.padding(10)
.background(Color(.systemGray4))
.cornerRadius(8)
.onTapGesture {
vm.toggleFav(item: thisBiase)
}
}
}
The ViewModel class...
import Foundation
import SwiftUI
#MainActor final class ViewModel: ObservableObject{
#Published var items = [Biase]()
#Published var showingFavs = false
#Published var savedItems: Set<Int> = [1, 7]
// Filter saved items
var filteredItems: [Biase] {
if showingFavs {
return items.filter { savedItems.contains($0.id) }
}
return items
}
private var BiasStruct: BiasData = BiasData.allBias
private var db = Database()
init() {
self.savedItems = db.load()
self.items = BiasStruct.biases
}
func sortFavs(){
withAnimation() {
showingFavs.toggle()
}
}
func contains(_ item: Biase) -> Bool {
savedItems.contains(item.id)
}
// Toggle saved items
func toggleFav(item: Biase) {
if contains(item) {
savedItems.remove(item.id)
} else {
savedItems.insert(item.id)
}
db.save(items: savedItems)
}
}
I really appreciate your help!
My initial thought was to have a bool var that, when true, would not actually change the favorite value until after the user left its detail view. Even if I could get that to work it's not ideal because if the user leaves the app in its detail view the favorite is not saved.
Yep that was a major problem with NavigationView. Its replacement: NavigationStack and .navigationDestination are designed to resolve it.

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)")
}
}
}
}

Set picker default no clickable value as title

I'm trying to develop a Picker with a field that corresponds to the title. The problem is that I'm not understanding how to use the title field of the Picker view.
This is the code. The problem is that the Picker is taking as title the string "Spain". Instead I want the title "Select country" which is visible until the user select a field.
struct CustomPicker: View {
#State private var selection = "Select country"
let colors = ["Spain", "France"]
var body: some View {
VStack(alignment: .leading, spacing: 4, content: {
HStack(spacing: 15) {
Picker("Select country", selection: $selection) {
ForEach(colors, id: \.self) {
Text($0)
}
}
.pickerStyle(DefaultPickerStyle())
}
.padding(.horizontal, 20)
})
.frame(height: 50)
.background(.white)
.cornerRadius(10)
.padding(.horizontal, 20)
}
}
What you're trying to do doesn't come standard with SwiftUI. You would have to custom make your UI for this (and that might not be hard). Depending on how much you're willing to compromise, you can have what you want with a slight tweak of your code. This is what a picker looks like within a List (as well as your Picker in the List).
To do this I modified your code slightly to include an enum for the countries.
enum Country: String, CaseIterable, Identifiable {
case spain, france, germany, switzerland
var id: Self { self }
}
struct CustomPicker: View {
#State private var selection: Country = .spain
var body: some View {
NavigationView {
List {
Picker("Select Country", selection: $selection) {
ForEach(Country.allCases, id: \.self) {
Text($0.rawValue.capitalized)
.tag($0)
}
}
Picker("Select Country", selection: $selection) {
ForEach(Country.allCases, id: \.self) {
Text($0.rawValue.capitalized)
.tag($0)
}
}
.pickerStyle(.menu)
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("Country Picker")
}
}
}

SwiftUI 2 Observable objects in one view which depend on each other

Thanks for all the support I have received, I trying to build an macos app that tags pdfs for machine learning purposes. I have followed Stanford SwiftUI course, and I want to create main view for my app that contains the document and to type a regex string to find in the document. The deal is I need to create a document chooser, to add documents to be analized, but I don't know how to deal with 2 view models in the same view. In fact one of those view models depend on the other one. The solution I found (not a solution a messy workaround) is to initialize the document manager as a separate view and use it as a navigation view, with a navigation link, but the look is horrible. I'll paste the code and explain it better.
This is the stores view
struct PDFTaggerDocumentStoreView: View {
#EnvironmentObject var store:PDFTaggerDocumentStore
var body: some View {
NavigationView {
List {
Spacer()
Text("Document Store").fontWeight(.heavy)
Button(action: {self.store.addDocument()}, label: {Text("Add document")})
Divider()
ForEach(store.documents){ doc in
NavigationLink(destination: PDFTaggerMainView(pdfTaggerDocument: doc)) {
Text(self.store.name(for: doc))
}
}
.onDelete { indexSet in
indexSet.map{self.store.documents[$0]}.forEach { (document) in
self.store.removeDocuments(document)
}
}
}
}
}
}
The main view.
struct PDFTaggerDocumentView: View {
#ObservedObject var document:PDFTaggerDocument
#State private var expression = ""
#State private var regexField = ""
#State private var showExpressionEditor = false
var body: some View {
VStack {
ZStack {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.stroke(Color.white, lineWidth: 0.5)
.frame(width: 600)
.padding()
VStack {
Text("Try expression ")
HStack {
TextField("Type regex", text: $document.regexString)
.frame(width: 200)
Image(nsImage: NSImage(named: "icons8-save-80")!)
.scaleEffect(0.3)
.onTapGesture {
self.showExpressionEditor = true
print(self.document.regexString)
print(self.regexField)
}
.popover(isPresented: $showExpressionEditor) {
ExpressionEditor().environmentObject(self.document)
.frame(width: 200, height: 300)
}
}
Picker(selection: $expression, label: EmptyView()) {
ForEach(document.expressionNames.sorted(by: >), id:\.key) { key, value in
Text(key)
}
}
Button(action: self.addToDocument, label: {Text("Add to document")})
.padding()
.frame(width: 200)
}
.frame(width:600)
.padding()
}
.padding()
Rectangle()
.foregroundColor(Color.white).overlay(OptionalPDFView(pdfDocument: document.backgroundPDF))
.frame(width:600, height:500)
.onDrop(of: ["public.file-url"], isTargeted: nil) { (providers, location) -> Bool in
let result = providers.first?.hasItemConformingToTypeIdentifier("public.file-url")
providers.first?.loadDataRepresentation(forTypeIdentifier: "public.file-url") { data, error in
if let safeData = data {
let newURL = URL(dataRepresentation: safeData, relativeTo: nil)
DispatchQueue.main.async {
self.document.backgroundURL = newURL
}
}
}
return result!
}
}
}
I'd like to be able to initialize both views models in the same view, and make the document view, be dependent on the document chooser model.
Is there a way I can do it?
Thanks a lot for your time.