How to write an array of values to Firestore with SwiftUI - swift

Currently I have a screen that fetches a list of "items" from an "items" Collection. The user is supposed to be able to select multiple items, and add them to a document within the "Movies" collection when a button is pressed. I can't seem to figure out how to write the function to write the list of these items to the Movie document.
Code examples below
This is the loop that fetches the items (working so far)
let movie: Movie
#State var selections: [String] = []
#ObservedObject private var viewModel = ItemsViewModel()
#State private var isSelected = false
#State var movieID = ""
var body: some View {
VStack {
ForEach(viewModel.items.indices, id: \.self) { i in
Button(action: { viewModel.items[i].isSelected.toggle() }) {
HStack {
if viewModel.items[i].isSelected {
Text(viewModel.items[i].name)
.foregroundColor(.white)
.font(.system(size: 16, weight: .bold))
Spacer()
Image(systemName: "checkmark")
.foregroundColor(ColourManager.pinkColour)
.font(.system(size: 16))
} else {
Text(viewModel.items[i].name)
.foregroundColor(Color(.systemGray))
.font(.system(size: 16))
Spacer()
}
}
.padding(.vertical, 10)
.padding(.horizontal)
}
Divider()
}
}
.onAppear() {
self.viewModel.fetchData()
}
Button calling function
Button(action: { self.addItems() }) {
HStack {
Text("Add Selected Items")
}
Function to write to Firestore
func addItems() {
let db = Firestore.firestore()
db.collection("movies").document(self.movieID).setData(["item": [self.selections]])
}
What I would like is the Firestore db to look like:
{
movieID: 12345,
movieName: "Goodfellas",
items: [
item: "item 1",
item: "item 2"
]
}
So far I have the movieID and movieName working, just can't figure out how to record the selections and write them as items. Any help would be greatly appreciated!

The answer depends on what you'd like the type of items field to be. The information you want to store could be stored either as an array of strings or as an object literal. Using JSON:
// Array of String
items: ["item1", "item2"]
// Object Lieral
items: {
"key1": "item1",
"key2": "item2"
}
From the code posted the variable selections is already an array of strings, so the first option would be as simple as:
db.collection("movies").document(self.movieID).setData(["item": self.selections])
As for the second one, keep in mind that the keys of object literals must be unique so you'd need to derive these keys for each item somehow, construct the object and add it as the value of the document items key. For further reference on adding data in Swift check the documentation

Related

Firebase setData not saving to Firestore

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

onDelete function for Ordered Dictionary - Cannot convert value of type 'IndexSet' to expected argument type > Int

I have an OrderedDictionary that I put into a List. How could I delete single elements from the dictionary using the onDelete function. At the moment, It is only printing the first element of the dictionary.
struct RecipeIngredients: View {
#State var test: OrderedDictionary <String, String> = ["ab": "AC", "this will be middle": "middle", "third": "third"]
private func listContent(for keys: [String]) -> some View {
ForEach(keys, id: \.self) { key in
HStack{
Text(key)
.font(.title2)
.foregroundColor(.green)
.fontWeight(.bold)
}
}
.onDelete { indexSet in
let key = keys[indexSet.first!]
self.test.removeValue(forKey: key)
}
}
var body: some View {
List{
listContent(for: Array(test.keys))
}
.listStyle(SidebarListStyle())
}
}
So finally fixed this one, I figured I'll post the solution here to help anyone who may have a similiar issue.
The issue was in my drawing of the view, I didn't call test[key], which now works and deletes according to the position of the dictionary.
#State var test: OrderedDictionary <String, String> = ["ab": "AC", "this will be middle": "middle", "third": "third"]
private func listContent(for keys: [String]) -> some View {
ForEach(keys, id: \.self) { key in
HStack{
Text(key)
.font(.title2)
.foregroundColor(.green)
.fontWeight(.bold)
Text(test[key]!)
}
}
.onDelete { indexSet in
let key = test.keys[indexSet.first!]
self.test.removeValue(forKey: key)
}
}
List{
self.listContent(for: Array(test.keys))
}
.listStyle(SidebarListStyle())
}

Swift add a button that deletes row from list that uses a foreach loop

I have a list that has a foreach loop and in every row I want to make a delete button that deletes the row.
I have an array of structs
struct Lists{
var title : String
var id = UUID()
}
and the array
#State var list = [Lists]()
and this is the list
List{
ForEach(list, id : \.id ){ item in
NavigationLink(destination:DetailView(word:item.title)){
HStack {
Text(item.title)
Spacer()
Button(action: {
self.list.remove(at: 0)
}){
Text("×")
}
}
}
}
}
I have set the remove(at:0) zero because I don't know how to get the index in the for loop.
The list:
I usually think is easier to use a count number instead.. like this:
List {
ForEach(0..<list.count, id : \.self ){ index in
NavigationLink(destination:DetailView(word: list[index].title)){
HStack {
Text(list[index].title)
Spacer()
Button(action: {
self.list.remove(at: index)
}){
Text("×")
}
}
}
}
}

Simple SwiftUI CRUD using structs rather than classes?

I have a complex data structure which uses value types (structs and enums), and I'm facing major issues getting basic CRUD to work. Specifically:
How best to "Re-bind" a value in a ForEach for editing by a child view
How to remove/delete a value
Rebinding
If I have an array of items as #State or #Binding, why isn't there a simple way to bind each element to a view? For example:
import SwiftUI
struct Item: Identifiable {
var id = UUID()
var name: String
}
struct ContentView: View {
#State var items: [Item]
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
TextField("name", text: $item) // 🛑 Cannot find '$item' in scope
}
}
}
}
Workaround
I've been able to work around this by introducing a helper function to find the correct index for the item within a loop:
struct ContentView: View {
#State var items: [Item]
func index(of item: Item) -> Int {
items.firstIndex { $0.id == item.id } ?? -1
}
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
TextField("name", text: $items[index(of: item)].name)
}
}
}
}
However, that feels clunky and possibly dangerous.
Deletion
A far bigger issue: how are you supposed to correctly delete an element? This sounds like such a basic question, but consider the following:
struct ContentView: View {
#State var items: [Item]
func index(of item: Item) -> Int {
items.firstIndex { $0.id == item.id } ?? -1
}
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
TextField("name", text: $items[index(of: item)].name)
Button( action: {
items.remove(at: index(of: item))
}) {
Text("Delete")
}
}
}
}
}
Clicking the "Delete" button on the first few items works as expected, but trying to Delete the last item results in Fatal error: Index out of range...
My particular use case doesn't map to a List, so I can't use the deletion helper there.
Reference types
I know that reference types make much of this easier, especially if they can conform to #ObservableObject. However, I have a massive, nested, pre-existing value type which is not easily converted to classes.
Any help would be most appreciated!
Update: Suggested solutions
Deleting List Elements from SwiftUI's list: The accepted answer proposes a complex custom binding wrapper. Swift is powerful, so it's possible to solve many problems with elaborate workarounds, but I don't feel like an elaborate workaround should be necessary to have a list of editable items.
Mark Views as "deleted" using State or a private variable, then conditionally hide them, to avoid out-of-bounds errors. This can work, but feels like a hack, and something that should be handled by the framework.
I confirm that more appropriate approach for CRUD is to use ObservableObject class based view model. And an answer provided by #NewDev in comments is a good demo for that approach.
However if you already have a massive, nested, pre-existing value type which is not easily converted to classes., it can be solved by #State/#Binding, but you should think about what/when/and how update each view and in each order - that is the origin of all such index out of bounds on delete issues (and some more).
Here is demo of approach of how to break this update dependency to avoid crash and still use value types.
Tested based on your code with Xcode 11.4 / iOS 13.4 (SwiftUI 1.0+)
struct ContentView: View {
#State var items: [Item] = [Item(name: "Name1"), Item(name: "Name2"), Item(name: "Name3")]
func index(of item: Item) -> Int {
items.firstIndex { $0.id == item.id } ?? -1
}
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
// separate dependent views as much as possible to make them as
// smaller/lighter as possible
ItemRowView(items: self.$items, index: self.index(of: item))
}
}
}
}
struct ItemRowView: View {
#Binding var items: [Item]
let index: Int
#State private var destroyed = false // internal state to validate self
var body: some View {
// proxy binding to have possibility for validation
let binding = Binding(
get: { self.destroyed ? "" : self.items[self.index].name },
set: { self.items[self.index].name = $0 }
)
return HStack {
if !destroyed { // safety check against extra update
TextField("name", text: binding)
Button( action: {
self.destroyed = true
self.$items.wrappedValue.remove(at: self.index)
}) {
Text("Delete")
}
}
}
}
}
Yes, it is not easy solution, but sometimes there are situations we need it.

SwiftUI core data, grouped list fetch result

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)