write (add) multiple array in firestore - swift

I try to add multiple array in firestore using struct codable in swiftui but when I add, it updates the same array added but i want add more array in my list...
as you can see in the image, every time I add a text, it updates my index "0" but I want to add multiple indexes, for example O, 1, 2... in my array" tags"
ViewModel
class DataManager: ObservableObject {
#Published var books = Book(tags: [Tag(title: "", color: "")])
func addBookNew(nom: String, prenom: String){
//guard let userID = Auth.auth().currentUser?.uid else {return}
let db = Firestore.firestore()
let collectionRef = db.collection("Users").document("TEST")
do {
let data = Book(tags: [Tag(title: nom, color: prenom)])
try collectionRef.setData(from: data)
}
catch {
print(error)
}
}
STRUCT BOOK
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
struct Book: Codable {
#DocumentID var id: String?
let tags: [Tag]
}
STRUCT TAG
import Foundation
import FirebaseFirestoreSwift
struct Tag: Codable, Hashable {
let title: String
let color: String
}
LIST VIEW
import SwiftUI
import Firebase
struct ListView: View {
#StateObject var dataManager = DataManager()
#State private var showPopUp = false
#EnvironmentObject var viewModel: AppViewModel
var books: [Book]
#State var nomTF = ""
#State var prenomTF = ""
#State var dismiss = false
var body: some View {
NavigationView{
VStack{
TextField("nom", text: $nomTF )
TextField("prenom", text: $prenomTF)
Button("save") {
dataManager.addBookNew(nom: nomTF, prenom: prenomTF)
}
List{
ForEach(dataManager.books.tags, id: \.self) { book in
HStack{
Text("T: \(book.title)")
Text("C: \(book.color)")
}
}
}
}.onAppear(){
dataManager.fetchtest()
}

The problem is here:
let data = Book(tags: [Tag(title: nom, color: prenom)])
try collectionRef.setData(from: data)
This code tells Firestore to set the tags field to the array that you specify in the call.
If you want to add unique items to existing array, you can use FieldValue.arrayUnion([...]).
let data = Book(tags: FieldValue.arrayUnion([Tag(title: nom, color: prenom)]))
try collectionRef.setData(from: data)
Note that this will only add the item if it is not already in the array. If you want to allow for duplicate items, you will have to read the array from the document first, then updates in your application code, and finally write the entire array back to the database.
Update: it seems your Book class can only accept Tag in its tags field, so you can't specify an FieldValue.arrayUnion there. It's probably easiest to then simply perform the union in the database call:
try collectionRef.updateData([
"tags": FieldValue.arrayUnion([Tag(title: nom, color: prenom)])
])
So instead of writing the entire book here, we're updating a single field in the document, and in there perform the array union.
washingtonRef.updateData([
"regions": FieldValue.arrayUnion(["greater_virginia"])
])

Related

Is it possible to make a customisable & reusable SwiftUI Form?

I want the to be able to create a Form in SwiftUI by creating a custom model and defining what fields are needed. Eventually, I want the User to be able to do this to make their own custom forms and data models, but for now I just want to reduce the code so that I can just quickly "Write" different types of Form and use a For loop to generate the form.
I.e. I have basically duplicate pages of code for different types of forms, which all work the same but just have different values and it seems like a waste, and I could just have some kind of single view and feed the data needed to that so I can reuse it, and eventually allow users to create their own custom forms in the future.
Here is what I have come up with so far as an example (Just playing around and trying to see what might be possible):
import SwiftUI
import Foundation
var fieldTypes = ["Field", "Slider", "List", "Date"...]
struct Worksheet: Codable, Identifiable, Hashable {
//data for populating Form
var id: String
var name: String?
var fields: [Field]
//data for saving model and values of form
var title: String?
var date: Date
var severity: Double
var trigger: String?
var description: String
var symptoms: [String]
var nInterpretation: String?
var rResponse: String
}
struct Field: Codable, Identifiable, Hashable {
var id: String
var type: String
var title: String
var description: String
var footer: String
}
class Dev_VM: ObservableObject {
var fields = [Field(id: "i", type: "Slider", title: "SliderTitle", description: "Description", footer: "Footer"), Field(id: "id2", type: "TextField", title: "TextFieldTitle", description: "Description", footer: "Footer")]
}
//Form view for data entry
struct Dev_FormView: View {
#Environment(\.presentationMode) var presentation
#ObservedObject var vm = Dev_VM()
static let taskDateFormat: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .full
dateFormatter.timeStyle = .short
dateFormatter.locale = Locale.current
return dateFormatter
}()
var body: some View {
Form {
//Section for selecting date.
for field in vm.fields {
switch field.type {
case "TextField":
Section(header: Text(field.title), footer: Text(field.footer)) {
ZStack {
TextEditor(text: $title)
.disabled(!editMode)
Text(title).opacity(0).padding(.all, 8)
}
}
case "Slider":
HStack {
Slider(value: $value, in: 0...100, step: 1)
.disabled(true)
Text("\(Int(severity))")
}
default:
print("Empty")
}
}
}
.navigationBarTitle("Dev_Playground", displayMode: .inline)
}
}
Seems that there are problems, for example on the for statement I get the error Closure containing control flow statement cannot be used with result builder 'ViewBuilder'
Another problem seems to be populating the values, as I will need #State variables, because the fields are dynamic, could I make these dynamic too?
Does anyone think that what I'm trying to do might be possible? Thank you!

SwiftUI: Cannot convert value of type 'String' to expected argument type 'Model' – Okay, but what is the expected argument?

I'm still new to SwiftUI and hope I will be able to explain my problem well.
I followed a tutorial on reading, writing, and deleting data from the Firestore Database in SwiftUI and it all worked. In the tutorial, there is only 1 view used, but I wanted to take it a step further and use further views:
List View (to show all items)
Form View (form with fields and function to add items)
Detail View (shows details of each item and function to delete item)
While I can add new items to my database through Form View and they also show up after going back to List View, when I try to implement the function to delete items in Detail View, XCode throws the error: Cannot convert value of type 'String' to expected argument type 'Model'
I understand I am not using the right parameter in my function, but failing to understand what the right one here is.
Model
import Foundation
struct Model: Identifiable {
var id: String
var name: String
var itemName: String
}
ItemListModel
import Foundation
import FirebaseCore
import FirebaseFirestore
class ItemListModel: ObservableObject {
#Published var list = [Model]()
#Published var name = [Model]()
…
func deleteItem(itemDelete: Model) {
// Get a reference to the database
let database = Firestore.firestore()
// Specifiy the document to delete. Data passed through the method.
database.collection("xxx").document(itemDelete.id).delete { error in
// Check for errors
if error == nil {
// No errors
// Update the UI from the main thread
DispatchQueue.main.async {
// Remove the item that was just deleted
self.list.removeAll { items in
// Check for the item to remove
return items.id == itemDelete.id
}
}
}
}
}
…
}
ItemList
My thinking was to include an ID to tell the function what to delete exactly, so I included it in this file and pass it to the Detail View. Not sure if I need to pass the ID at this point though.
import SwiftUI
struct ItemList: View {
#EnvironmentObject var model: ItemListModel
#State var name = ""
#State var itemName = ""
#State var itemId = ""
var body: some View {
…
ForEach(model.list) { item in
NavigationLink(destination: Detail(name: item.name, itemName: item.itemName, itemId: item.id)) {
HStack {
Text(item.name)
Text(item.itemName)
}
}
}
}
}
Detail
And this is the view where XCode throws the error message Cannot convert value of type 'String' to expected argument type 'Model'.
import SwiftUI
struct Detail: View {
#EnvironmentObject var model: ItemListModel
var name: String
var itemName: String
var itemId: String
var body: some View {
VStack {
Text(name)
Text(itemName)
Button {
// delete (hopefully) the current item
model.deleteItem(itemDelete: itemId) // XCode throws error Cannot convert value of type 'String' to expected argument type 'Model'.
} label: {
Text("Delete")
}
…
}
}
}
Thank you for reading through all of this and I truly hope someone can help me and my confused brain here!
The expected type is Model. -> deleteItem(itemDelete: Model)
The easy solution is to pass a Model instance rather than the three strings
struct Detail: View {
#EnvironmentObject var model: ItemListModel
let item: Model
var body: some View {
VStack {
Text(item.name)
Text(item.itemName)
Button {
model.deleteItem(itemDelete: item)
} label: {
Text("Delete")
}
…
}
}
}
and in ItemList replace
NavigationLink(destination: Detail(name: item.name, itemName: item.itemName, itemId: item.id)) {
with
NavigationLink(destination: Detail(item: item)) {

SwiftUI - Why won't my #Published variable update within my #ObservedObject call?

In essence, I'm learning by creating a simple app that takes an ISBN and returns the book information. I've created a view file to display this information along with a text entry for capturing the ISBN. The logic side of things as pertaining to getting the information from the internet is fine, however, when I try to update the view with the modified variables, it only prints them as declared, and NOT redrawing them after modification. Any advice would be appreciated. Thanks!
This specific example references #Published var test = "Testing 1 2 3 as the default, and after the search, the var is modified to test = modified text which is then printed in the text view of TestView.swift. The print statement correctly displays the modified text, however it does not update in the view in which it is passed. The goal is to pass the final dictionary, but for testing purposes I'm using a simple string variable.
TestFile.swift
import Foundation
import SwiftyXMLParser
import SwiftUI
class getdata: ObservableObject {
#Published var outputDict: [String:String] = [:]
#Published var test = "Testing 1 2 3"
.
.
//code to grab xml from url
.
.
func parseData(input: String) {
var title: String?
var author: String?
let xml = try! XML.parse(input)
if let text = xml["GoodreadsResponse", "book", "title"].text {
title = text
outputDict["title"] = title
}
if let text = xml["GoodreadsResponse", "book", "authors", "author", "name"].text {
author = text
outputDict["author"] = author
}
print("Title: \(outputDict["title"]!), Author: \(outputDict["author"]!)")
test = "modified text"
print(test)
}
}
TestView.swift
import SwiftUI
struct testView: View {
#State var textEntry: String = ""
#ObservedObject var viewModel: getdata
var body: some View {
let try1 = getdata()
VStack {
TextField("Enter ISBN", text: $textEntry)
.padding()
Button(action: {try1.getData(bookID: textEntry)}, label: {
Text("Search")
})
Text("Title: \(self.viewModel.test)")
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView(viewModel: getdata())
}
}
Although I don't think your example compiles, and you haven't provided the definitions of some of the functions, I can point you at one error:
Inside the body of the view you are creating a new Observable object:
let try1 = getdata()
And your Button calls messages on this:
Button(action: {try1.getData(bookID: textEntry)}...
But your TextView reads from the original viewModel:
Text("Title: \(self.viewModel.test)")
You are interacting with try1, but deriving your view from viewModel. They are separate objects.

Binding Array of Structs with Strings in an Array of Structs

I'm new to Swift so I hope this isn't something really silly. I'm trying to build an array of Structs, and one of the parameters is another Array with another Struct in it. I'm not sure if there is a better way, but I thought I was making really good progress right up till I tried to edit the embedded Struct. In it's simplified form it looks like this ...
struct Group: Identifiable, Codable {
var id = UUID()
var name: String
var number: Int
var spaces: Bool
var businesses: [Business]
}
struct Business: Identifiable, Codable {
var id = UUID()
var name: String
var address: String
var space: Int
var enabled: Bool
}
These are used in a class with an Observable var that stored in User Defaults
class GroupSettings: ObservableObject {
#Published var groups = [Group]() {
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(groups), forKey: "groups")
}
}
init() {
if let configData = UserDefaults.standard.value(forKey: "groups") as? Data {
if let userDefaultConfig = try?
PropertyListDecoder().decode(Array<Group>.self, from: configData){
groups = userDefaultConfig
}
}
}
}
Its passed in to my initial view and then I'm wanting to make an "Edit Detail" screen. When it gets to the edit detail screen, I can display the Business information in a Text display but I can't get it to working a TextField, it complains about can't convert a to a Binding, but the name from the initial Struct works fine, similar issues with the Int ...
I pass a Group from the first view which has the array of Groups in to the detail screen with the #Binding property ...
#Binding var group: Group
var body: some View {
TextField("", text: $group.name) <---- WORKS
List {
ForEach(self.group.businesses){ business in
if business.enabled {
Text(business.name) <---- WORKS
TextField("", business.address) <---- FAILS
TextField("", value: business.space, formatter: NumberFormatter()) <---- FAILS
} else {
Text("\(business.name) is disabled"
}
}
}
}
Hopefully I've explained my self well enough, and someone can point out the error of my ways. I did try embedding the 2nd Struct inside the first but that didn't help.
Thanks in advance!
You could use indices inside the ForEach and then still use $group and accessing the index of the businesses via the index like that...
List {
ForEach(group.businesses.indices) { index in
TextField("", text: $group.businesses[index].address)
}
}
An alternative solution may be to use zip (or enumerated) to have both businesses and its indices:
struct TestView: View {
#Binding var group: Group
var body: some View {
TextField("", text: $group.name)
List {
let items = Array(zip(group.businesses.indices, group.businesses))
ForEach(items, id: \.1.id) { index, business in
if business.enabled {
Text(business.name)
TextField("", text: $group.businesses[index].address)
} else {
Text("\(business.name) is disabled")
}
}
}
}
}

Why is my SwiftUI List appending all objects again instead of updating existing objects?

I'm reading data from Firestore using .addSnapshotListener and parsing it to a custom model Thought
For each document in my Firestore collection I append a new Thought object to a #Published var thoughts and iterate through thoughts in a List.
struct Thought: Identifiable {
public var id: String?
public var name: String
public var thought: String
public var color: String
}
class Observer: ObservableObject {
#Published var thoughts = [Thought]()
init(){
self.thoughts.removeAll()
let db = Firestore.firestore()
db.collection("thoughts")
.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
for document in documents {
var thoughtModel = Thought(id: "", name: "", thought: "", color: "")
thoughtModel.name = document.data()["name"] as! String
thoughtModel.thought = document.data()["thought"] as! String
thoughtModel.color = document.data()["color"] as! String
self.thoughts.append(thoughtModel)
}
}
}
}
struct ThoughtsView: View {
#ObservedObject var observer = Observer()
var body: some View {
VStack {
List {
ForEach(self.observer.thoughts, id: \.name) { thought in
ThoughtCard(color: thought.color,
thought: thought.thought,
name: thought.name)
}
}
}
}
}
When I make changes or add a document in my Firestore collection all objects are appended to my List instead of my List just being updated like I expect. In other words, if I have 3 items in my List, and I change a value in one of the Firestore documents I end up with 6 items in my List consisting of the original 3, and a duplicate of the original 3 (modified).
How do I properly update my List?
all objects are appended to my List instead of my List just being updated like I expect.
Your expectation is correct, but ...
#ObservedObject var observer = Observer()
... your observer is created only once and kept outside of ThoughtsView, so even if a view is reconstructed on update the observed object is used the same, so ...
init(){
self.thoughts.removeAll()
this code is called only once, because it is in constructor, and, actually, useless. However this part
self.thoughts.append(thoughtModel)
... is called on every update, because it is in .addSnapshotListener. As a result you see what you see - continuous appending.
Solution. From the logic of the code I assume your meant this place of container cleanup
self.thoughts.removeAll()
for document in documents {