I'm trying to give the option to the user in my app to divide the data in a custom folder.
I have created the following model describing the folder:
class FolderModel : Identifiable , Codable{
var nameFolder : String
var vectorData : [GenAirportModel]
init(nameFolder: String, vectorData: [GenAirportModel] ) {
self.nameFolder = nameFolder
self.vectorData = vectorData
}
}
with the following function via a textfield I add the new folder
func newFolder(name: String) {
let newFolder: FolderModel = FolderModel(nameFolder: name, vectorData: [basicAirport])
folderVector.append(newFolder)
salva()
}
The issue is, every time the user creates a new folder, the function above put inside vectorData a basic airport.
Unfortunately when the user creates the folder not yet decide what to put inside vectorData I would like that vector data be optional or empty when the user creates the new folder.
I have tried this:
class FolderModel : Identifiable , Codable{
var nameFolder : String
var vectorData : [GenAirportModel]?
init(nameFolder: String, vectorData: [GenAirportModel]? = [] ) {
self.nameFolder = nameFolder
self.vectorData = vectorData
}
}
and in the view that list the data I put this:
VStack{
if addFolder {
ADDFolder(fm: self.fm, isDiplayed: $addFolder)
}
NavigationView{
List {
ForEach(self.fm.folderVector) { folder in
NavigationLink(destination:
VStack{
if folder.vectorData!.isEmpty {
Text("no data")
} else {
List {
ForEach(folder.vectorData!) { item in
Text(item.airportData.aptICAO)
}
}
}
//
}
) {
HStack{
Image(systemName: "folder")
Text(folder.nameFolder)
}
}
}
}.navigationBarTitle(Text("Folder List"), displayMode: .large)
.navigationBarItems(trailing:
Button(action: {
self.addFolder.toggle()
}, label: {
Image(systemName: "plus")
})
)
}
}
But app crashes when I try to list because it found the empty vector.
How can I create the optional vectorData, the user append that data after it create the folder name.
thanks
I assume it is here
if folder.vectorData != nil { // << check explicitly for nil
Text("no data")
} else {
List {
ForEach(folder.vectorData!) { item in
Text(item.airportData.aptICAO)
}
}
}
Related
Attempting to update values in my database. I checked and the type of ema.updatedIngredients that I'm passing to saveFunctions is a valid type in firestore.
The saveRecipe function works fine if I would use updateData. It deletes the documents as expected, but I know I'm pointing to the right document.
I can also add documents fine, it is when I am deleting an ingredients and updating the data where it doesn't work.
Save Recipe function to Firebase
func saveRecipe(ingredientList: [String: String], currentRecipe: String){
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
return
}
FirebaseManager.shared.firestore
.collection("users")
.document(uid)
.collection("userRecipes")
.whereField("recipeID", isEqualTo: currentRecipe)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
FirebaseManager.shared.firestore
.collection("users")
.document(uid)
.collection("userRecipes")
.document(document.documentID)
.setData(["ingredientItem" : ingredientList], merge: true)
print("Updated Recipe")
}
}
}
}
Where I call saveFunction
.toolbar{
ToolbarItem(placement: .navigationBarTrailing){
Button(action: {
ema.editMode.toggle()
//if user is saving when complete is on the button
if !ema.editMode {
//saving to firestore
rm.saveRecipe(ingredientList: ema.updatedIngredients, currentRecipe: recipeID)
}
}){
HStack{
Image(systemName: !ema.editMode ? "pencil.circle" : "")
.foregroundColor(.black)
Text(!ema.editMode ? "Edit" : "Complete")
.foregroundColor(.black)
}
}
}
}
}
}
My View
struct RecipeIngredients: View {
#State private var sheetMode: SheetMode = .quarter
#State private var sizing = ""
#State private var description = ""
#ObservedObject var rm = RecipeLogic()
#ObservedObject var ema: EditModeActive
#Binding var currentRecipeID: String
#Binding var ingredients: [String: String]
//turn into Ordered Dictionary so I can grab ingredients key
func turnIntoDictionary(regularDictionary: [String: String]) -> OrderedDictionary <String, String>{
var dict = OrderedDictionary <String, String> (
uniqueKeys: regularDictionary.keys,
values: regularDictionary.values
)
dict.sort()
return dict
}
private func listContent(for keys: [String]) -> some View {
ForEach(keys, id: \.self) { key in
HStack{
Text(key)
.font(.title2)
.foregroundColor(.green)
.fontWeight(.bold)
Text(turnIntoDictionary(regularDictionary: ingredients)[key] ?? "default")
.font(.title3)
}
}
.onDelete { indexSet in
if ema.editMode{
let key = turnIntoDictionary(regularDictionary: ingredients).keys[indexSet.first!]
self.ingredients.removeValue(forKey: key)
ema.updatedIngredients = ingredients
}
}
}
var body: some View {
ZStack{
VStack{
if ema.editMode{
HStack{
TextField("ex. 1 cup", text: $sizing)
.font(.body)
.padding(.leading, 30)
TextField("ex. Chicken Breast", text: $description)
.font(.body)
}
.padding(.top, 25) //set to give space from ingredient/direction section
Button(action: {
if (sizing != "" && description != ""){
ingredients[sizing] = description
ingredients[sizing] = description
ema.updatedIngredients[sizing] = description
sizing = ""
description = ""
}
})
{
Image(systemName: "plus.circle.fill")
.foregroundColor(.blue)
.padding(.leading, 20)
.padding(.top, 20)
.opacity(!sizing.isEmpty && !description.isEmpty ? 1.0 : 0.5)
Spacer()
}
.padding(.top, -10)
.padding(.bottom, 10)
}
List{
self.listContent(for: Array(turnIntoDictionary(regularDictionary: ingredients).keys))
}
.onAppear{
ema.updatedIngredients = ingredients
}
.listStyle(SidebarListStyle())
}
}
}
}
EDIT: For further confusion, if I print right before calling the function, it prints the correct keys/values.
print(ema.updatedIngredients)
rm.saveRecipe(ingredientList: ema.updatedIngredients, currentRecipe: recipeID)
Almost ashamed to admit the simplicity of this solution. Once I went from setData to updateData it worked as expected.
Figured I would share in case someone runs into a similar issue.
There are two methods for updating:
1 . set() - Sets data on the document, overwriting any existing data. If the document does not yet exist, it will be created.
2 . update() - Updates data on the document. Data will be merged with any existing document data. If no document exists yet, the update will fail.
== In flutter ==
To update a value in the document
var collection =
FirebaseFirestore.instance.collection('collection');
collection
.doc('doc_id')
.update({'key' : 'value'}) // <-- Updated data
.then((_) => print('Success'))
.catchError((error) => print('Failed: $error'));
To add a new value to the existing document.
var collection =
FirebaseFirestore.instance.collection('collection');
collection
.doc('doc_id')
.set(yourData, SetOptions(merge: true)); // <-- Set merge to true.
So in your case update() is more suited than set()
I don't know how in Swift, but in JavaScript you need to pass {merge: true} as a second positional argument into .setData(), and it will update the document like .updateData() does.
Example:
...
.setData(["ingredientItem": ingredientList], {merge: true})
I'm getting a weird memory leak in SwiftUI when using List and id: \.self, where only some of the items are destroyed. I'm using macOS Monterey Beta 5.
Here is how to reproduce:
Create a new blank SwiftUI macOS project
Paste the following code:
class Model: ObservableObject {
#Published var objs = (1..<100).map { TestObj(text: "\($0)")}
}
class TestObj: Hashable {
let text: String
static var numDestroyed = 0
init(text: String) {
self.text = text
}
static func == (lhs: TestObj, rhs: TestObj) -> Bool {
return lhs.text == rhs.text
}
func hash(into hasher: inout Hasher) {
hasher.combine(text)
}
deinit {
TestObj.numDestroyed += 1
print("Deinit: \(TestObj.numDestroyed)")
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
NavigationView {
List(model.objs, id: \.self) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
}
}
Run the app, and press the "Remove half" button. Keep pressing it until all the items are gone. However, if you look at the console, you'll see that only 85 items have been destroyed, while there were 99 items. The Xcode memory graph also supports this.
This seems to be caused by the id: \.self line. Removing it and switching it out for id: \.text fixes the problem.
However the reason I use id: \.self is because I want to support multiple selection, and I want the selection to be of type Set<TestObj>, instead of Set<UUID>.
Is there any way to solve this issue?
If you didn't have to use selection in your List, you could use any unique & constant id, for example:
class TestObj: Hashable, Identifiable {
let id = UUID()
/* ... */
}
And then your List with the implicit id: \.id:
List(model.objs) { obj in
Text(obj.text)
}
This works great. It works because now you are no longer identifying the rows in the list by a reference type, which is kept by SwiftUI. Instead you are using a value type, so there aren't any strong references causing TestObjs to not deallocate.
But you need selection in List, so see more below about how to achieve that.
To get this working with selection, I will be using OrderedDictionary from Swift Collections. This is so the list rows can still be identified with id like above, but we can quickly access them. It's partially a dictionary, and partially an array, so it's O(1) time to access an element by a key.
Firstly, here is an extension to create this dictionary from the array, so we can identify it by its id:
extension OrderedDictionary {
/// Create an ordered dictionary from the given sequence, with the key of each pair specified by the key-path.
/// - Parameters:
/// - values: Every element to create the dictionary with.
/// - keyPath: Key-path for key.
init<Values: Sequence>(_ values: Values, key keyPath: KeyPath<Value, Key>) where Values.Element == Value {
self.init()
for value in values {
self[value[keyPath: keyPath]] = value
}
}
}
Change your Model object to this:
class Model: ObservableObject {
#Published var objs: OrderedDictionary<UUID, TestObj>
init() {
let values = (1..<100).map { TestObj(text: "\($0)")}
objs = OrderedDictionary<UUID, TestObj>(values, key: \.id)
}
}
And rather than model.objs you'll use model.objs.values, but that's it!
See full demo code below to test the selection:
struct ContentView: View {
#StateObject private var model = Model()
#State private var selection: Set<UUID> = []
var body: some View {
NavigationView {
VStack {
List(model.objs.values, selection: $selection) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
.onChange(of: selection) { newSelection in
let texts = newSelection.compactMap { selection in
model.objs[selection]?.text
}
print(texts)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
EditButton()
}
}
}
}
}
Result:
Currently, I am using #State for an Array of SKProducts
#State var products = [SKProduct]()
var body: some View {
ScrollView(.horizontal){
LazyHStack {
if products.count > 0 {
ForEach(products.indices) { index in
let product = self.products[index]
// ProductCell(product: product)
}
ForEach(1...products.count, id: \.self) { count in
let product = $products[count]
// ProductCell(product: Binding.constant(products[count]))
// ProductCell(name: <#Binding<String>#>).padding()
}
}
}
}.onAppear(perform: {
Purchases.shared.offerings { (offerings, error) in
if let tempPackages = offerings?.current?.availablePackages {
for package in tempPackages {
products.append(package.product)
}
}
}
})
}
I am trying to pass off an individual product to another view below:
ForEach(1...products.count, id: \.self) { count in
let product = $products[count]
}
However "product" is considered an "error type"
I am new to SwiftUI and I cannot for the life of me figure out what I am doing wrong. Thank you for your time and consideration.
products is an array, $products is a binding.
products[0] is the first element of an array. $products[0] means nothing because a binding doesn't have a subscript accessor.
Remove the $.
I'm building a game where players (locally) need to describe a number words to their teammates, who in turn guess as many words as possible within a given time limit. This means I have to let them create teams and add players to those teams.
I've set up Core Data and created a SwiftUI view for creating teams and adding players to those teams: this works. I'm now building a view to edit the team properties, but I'm struggling with data validation using the .disabled modifier. I want to make sure that the 'Save' button gets disabled when less than 2 people are in a team and when any of the fields (team name or any of player names) are empty.
What happens is that when editing the team name (a direct property of the Team entity), my .disabled modifier gets triggered as planned, and thus I can check whether it's empty or not. However, when editing any of the player names, the function passed to the .disabled modifier does not trigger, so I can't check if the player name is empty or not.
Below is my code:
struct EditTeamsView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var existingTeam: Team
var body: some View {
ZStack {
Color.init(UIColor.systemGroupedBackground)
.edgesIgnoringSafeArea(.all)
VStack {
Form {
// Team name textfield
Section(footer: Text("A team needs a minimum of 2 players, and can have a maximum of 5 players.")) {
TextField("Team name...", text: $existingTeam.name ?? "")
}
Section {
ForEach(existingTeam.playerArray, id: \.self) { player in
TextField("Player name...", text: Binding(
get: {
player.wrappedName
}, set: { newValue in
player.name = newValue
}))
}
.onDelete(perform: deletePlayers)
}
}
StockFullWidthButton(action: {
saveChanges()
self.presentationMode.wrappedValue.dismiss()
}, label: "Save Changes")
.disabled(!isInputValid())
}
}
.navigationBarTitle("Edit Team", displayMode: .inline)
}
func deletePlayers(at offsets: IndexSet) {
for index in offsets {
let playerToRemove = existingTeam.playerArray[index]
existingTeam.removeFromPlayers(playerToRemove)
}
}
func isInputValid() -> Bool {
print("Input is checked")
guard !existingTeam.name!.isEmpty else { return false }
guard existingTeam.playerArray.count >= 2 else { return false }
for player in existingTeam.playerArray {
if player.name!.isEmpty {
return false
}
}
return true
}
func saveChanges() {
if managedObjectContext.hasChanges {
try? self.managedObjectContext.save()
}
}
}
For the team name TextField binding I used this operator overload (taken from another post on SO). This works nicely for the team name textfield, but doesn't work for the player names.
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: { lhs.wrappedValue = $0 }
)
}
That's why I used a custom binding for the player names textfields:
Binding(get: { player.wrappedName }, set: { newValue in player.name = newValue })
Any idea on how to fix this? Thanks in advance!
If you want to update view on some condition you need to have explicit state for that and manage that state manually. In provided code you rely that view will be validated on refresh, but refresh does not happen because no dependency changed.
Here is possible solution - to have explicit state for bad input and trigger it explicitly when required condition changed
#ObservedObject var existingTeam: Team
#State private var isBadInput = false // << this !!
var body: some View {
ZStack {
Color.init(UIColor.systemGroupedBackground)
.edgesIgnoringSafeArea(.all)
VStack {
Form {
// Team name textfield
Section(footer: Text("A team needs a minimum of 2 players, and can have a maximum of 5 players.")) {
TextField("Team name...", text: $existingTeam.name ?? "")
.onReceive(existingTeam.$name) { _ in
self.isBadInput = !isInputValid() // << updated !!
}
}
Section {
ForEach(existingTeam.playerArray, id: \.self) { player in
TextField("Player name...", text: Binding(
get: {
player.wrappedName
}, set: { newValue in
player.name = newValue
}))
.onReceive(player.objectWillChange) { _ in
self.isBadInput = !isInputValid() // << updated !!
}
}
.onDelete(perform: deletePlayers)
}
}
StockFullWidthButton(action: {
saveChanges()
self.presentationMode.wrappedValue.dismiss()
}, label: "Save Changes")
.disabled(isBadInput) // << used !!
}
}
.navigationBarTitle("Edit Team", displayMode: .inline)
}
using core data im storing some airport and for every airport i'm storing different note
I have created the entity Airport and the entity Briefing
Airport have 1 attribute called icaoAPT and Briefing have 4 attribute category, descript, icaoAPT, noteID
On my detailsView I show the list all the noted related to that airport, I managed to have a dynamic fetch via another view called FilterList
import SwiftUI
import CoreData
struct FilterLIst: View {
var fetchRequest: FetchRequest<Briefing>
#Environment(\.managedObjectContext) var dbContext
init(filter: String) {
fetchRequest = FetchRequest<Briefing>(entity: Briefing.entity(), sortDescriptors: [], predicate: NSPredicate(format: "airportRel.icaoAPT == %#", filter))
}
func update(_ result : FetchedResults<Briefing>) ->[[Briefing]]{
return Dictionary(grouping: result) { (sequence : Briefing) in
sequence.category
}.values.map{$0}
}
var body: some View {
List{
ForEach(update(self.fetchRequest.wrappedValue), id: \.self) { (section : Briefing) in
Section(header: Text(section.category!)) {
ForEach(section, id: \.self) { note in
Text("hello")
/// Xcode error Cannot convert value of type 'Text' to closure result type '_'
}
}
}
}
}
}
on this view I'm try to display all the section divided by category using the func update...
but Xcode give me this error , I can't understand why..Cannot convert value of type 'Text' to closure result type '_'
fore reference I list below my detailsView
import SwiftUI
struct DeatailsView: View {
#Environment(\.managedObjectContext) var dbContext
#Environment(\.presentationMode) var presentation
#State var airport : Airport
#State var note = ""
#State var noteTitle = ["SAFTY NOTE", "TAXI NOTE", "CPNOTE"]
#State var notaTitleSelected : Int = 0
#State var notaID = ""
var body: some View {
Form{
Section(header: Text("ADD NOTE Section")) {
TextField("notaID", text: self.$notaID)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("add Note descrip", text: self.$note)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Picker(selection: $notaTitleSelected, label: Text("Class of Note")) {
ForEach(0 ..< noteTitle.count) {
Text(self.noteTitle[$0])
}
}
HStack{
Spacer()
Button(action: {
let nota = Briefing(context: self.dbContext)
nota.airportRel = self.airport
nota.icaoAPT = self.airport.icaoAPT
nota.descript = self.note
nota.category = self.noteTitle[self.notaTitleSelected]
nota.noteID = self.notaID
do {
try self.dbContext.save()
debugPrint("salvato notazione")
} catch {
print("errore nel salva")
}
}) {
Text("Salva NOTA")
}
Spacer()
}
}
Section(header: Text("View Note")) {
FilterLIst(filter: airport.icaoAPT ?? "NA")
}
}
}
}
thanks for the help
This is because you try to iterate over a single Briefing object and a ForEach loop expects a collection:
List {
ForEach(update(self.fetchRequest.wrappedValue), id: \.self) { (section: Briefing) in
Section(header: Text(section.category!)) {
ForEach(section, id: \.self) { note in // <- section is a single object
Text("hello")
/// Xcode error Cannot convert value of type 'Text' to closure result type '_'
}
}
}
}
I'd recommend you to extract the second ForEach to another method for clarity. This way you can also be sure you're passing the argument of right type ([Briefing]):
func categoryView(section: [Briefing]) -> some View {
ForEach(section, id: \.self) { briefing in
Text("hello")
}
}
Note that the result of your update method is of type [[Briefing]], which means the parameter in the ForEach is section: [Briefing] (and not Briefing):
var body: some View {
let data: [[Briefing]] = update(self.fetchRequest.wrappedValue)
return List {
ForEach(data, id: \.self) { (section: [Briefing]) in
Section(header: Text("")) { // <- can't be `section.category!`
self.categoryView(section: section)
}
}
}
}
This also means you can't write section.category! in the header as the section is an array.
You may need to access a Briefing object to get a category:
Text(section[0].category!)
(if you're sure the first element exists).
For clarity I specified types explicitly. It's also a good way to be sure you always use the right type.
let data: [[Briefing]] = update(self.fetchRequest.wrappedValue)
However, Swift can infer types automatically. In the example below, the data will be of type [[Briefing]]:
let data = update(self.fetchRequest.wrappedValue)