delete element from cloud firestore array in swiftui - swift

I'm trying to remove an element (only one) from an array using arrayRemove() but all elements in my "note" array are removed
Note struct:
struct Notes: Identifiable {
var id = UUID().uuidString
var nom: [String]
}
Category struct:
struct Categorys: Identifiable {
var id = UUID().uuidString
var idDoc: String
var note: Notes
}
viewModel:
class DataManager: ObservableObject {
#Published var note: [Notes] = []
#Published var cat: [Categorys] = []
func deleteFields2(cat: Categorys) {
let db = Firestore.firestore()
guard let userID = Auth.auth().currentUser?.uid else {return}
let note = cat.note.nom
db.collection("Users").document(userID).collection("Categorys").document(cat.idDoc).updateData([
"note": FieldValue.arrayRemove(note)
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}

To remove an item from the note array, you need to pass the exact value of that array item to arrayRemove. Your code passes an array to arrayRemove, so the code will remove any array item that is an array with the value you specify - which doesn't exist in the screenshots.
To remove the string item from the array, pass a string value to arrayRemove. For example:
db.collection("Users").document(userID).collection("Categorys").document(cat.idDoc).updateData([
"note": FieldValue.arrayRemove("cheese")
])

Related

How to make ForEach loop for a Codable Swift Struct's Dictionary (based on Firestore map)

I am trying to do a ForEach loop that lists all of the social medias a user might have. This would be on a scrollable list, the equivalent of music streaming apps have a list of all the songs you save in your library. The user's social medias list is in reality a dictionary.
My MainViewModel class implements all of the Firebase functionality. See below.
class MainViewModel: ObservableObject {
#Published var errorMessage = ""
#Published var user: User?
init() {
DispatchQueue.main.async {
self.isUserCurrentlyLoggedOut = FirebaseManager.shared.auth.currentUser?.uid == nil
}
readCodableUserWithMap()
}
#Published var isUserCurrentlyLoggedOut = false
func handleSignOut() {
isUserCurrentlyLoggedOut.toggle()
try? FirebaseManager.shared.auth.signOut()
}
func readCodableUserWithMap() {
guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {
self.errorMessage = "Could not find firebase uid"
print("FAILED TO FIND UID")
return
}
let userID = uid
let docRef = FirebaseManager.shared.firestore.collection("users").document(userID)
docRef.getDocument { (document, error) in
if let err = error {
print(err.localizedDescription)
return
}
if let doc = document {
let user = try! doc.data(as: User.self)
if let mappedField = user.socials {
mappedField.forEach { print($0.key, $0.value) }
}
}
}
}
}
readCodableUserWithMap() is supposed to initialize my codable struct, which represents a user. See below.
struct User: Identifiable, Codable {
#DocumentID var id: String?
var socials: [String: String]?
var uid, email, name, bio, profileImageUrl: String?
var numSocials, followers, following: Int?
}
QUESTION AT HAND: In my Dashboard View, I am trying to have a list of all the social medias a user can have. I can't manage to make a ForEach loop for my user.
I do:
ForEach(vm.user?.socials.sorted(by: >), id: \.key) { key, value in
linkDisplay(social: key, handler: value)
.listRowSeparator(.hidden)
}.onDelete(perform: delete)
This gives me the following errors:
Value of optional type '[Dictionary<String, String>.Element]?' (aka 'Optional<Array<(key: String, value: String)>>') must be unwrapped to a value of type '[Dictionary<String, String>.Element]' (aka 'Array<(key: String, value: String)>')
Value of optional type '[String : String]?' must be unwrapped to refer to member 'sorted' of wrapped base type '[String : String]'
I have tried coalescing using ?? but that doesn't work, although I assume I am doing something wrong. Force-unwrapping is something I tried but it made the app crash when it was an empty dictionary.
Just in case, here is the database hierarchy: firebase hierarchy
TLDR: HOW can I make a ForEach loop to list all of my user's medias?
You have both vm.user and socials as optionals.
The ForEach loop requires non-optionals, so
you could try the following approach to unwrap those for the ForEach loop.
if let user = vm.user, let socials = user.socials {
ForEach(socials.sorted(by: > ), id: \.key) { key, value in
linkDisplay(social: key, handler: value)
.listRowSeparator(.hidden)
}.onDelete(perform: delete)
}

How do you pass data dynamically is a Swift array?

Im creating a tinder like swipe app and I need a new CardData(name: "", age: "") to be created depending on how many profiles I pass through from my database. The number of cards will change. I need the number of cards created to match the the value of the results default. I have looked for the solution for a while and can't find it anywhere.
import UIKit
var nameArrayDefault = UserDefaults.standard.string(forKey: "nameArray")!
var ageArrayDefault = UserDefaults.standard.string(forKey: "ageArray")!
var nameArray = nameArrayDefault.components(separatedBy: ",")
var ageArray = ageArrayDefault.components(separatedBy: ",")
var results = UserDefaults.standard.string(forKey: "results")!
struct CardData: Identifiable {
let id = UUID()
let name: String
let age: String
static var data: [CardData] {[
CardData(name: “\(nameArray[0])”, age: “\(ageArray[0])”),
CardData(name: “\(nameArray[1])”, age: “\(ageArray[1])"),
CardData(name: “\(nameArray[2])”, age: “\(ageArray[2])”)
]}
}
You should initiate the array of CardData objects only the first time and update it after that. You can do the following
var _data = [CardData]()
var data: [CardData] {
if _data.isEmpty() {
self.initiateData()
}
return _data
}
// Initiate data for the first time only
func initiateData() -> [CardData] {
// Check that nameArray has the same size as ageArray
// If the size is different then the data are not valid.
guard nameArray.count == ageArray.count else {
return []
}
// For each of names inside nameArray create the corresponding CardData object
self.nameArray.forEach({ (index, item)
self._data.append(CardData(name: item, age: ageArray[index]))
})
}

DocumentId wrapper is not working properly

I am using #DocumentId in my data struct:
struct UserProfile : Codable, Identifiable {
#DocumentID var id: String? = UUID().uuidString
}
When I try to access the id I get a random string of letters (04F9C67E-C4A5-4870-9C22-F52C7F543AA5) Instead of the name of the document in firestore (GeG23o4GNJt5CrKEf3RS)
To access the documentId I am using the following code in an ObservableObject:
class Authenticator: ObservableObject {
#Published var currentUser: UserProfile = UserProfile()
#Published var user: String = ""
func getCurrentUser(viewModel: UsersViewModel) -> String {
guard let userID = Auth.auth().currentUser?.uid else {
return ""
}
viewModel.users.forEach { i in
if (i.userId == userID) {
currentUser = i
}
}
print("userId \(currentUser.id ?? "no Id")")
print("name \(currentUser.name)")
return userID
}
Where currentUser is a UserProfile struct. currentUser.name returns the proper value. What am I doing wrong?
currentUser is a member of an array populated using the following method:
func fetchData() {
db.collection("Users").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.users = documents.compactMap { (queryDocumentSnapshot) -> UserProfile? in
return try? queryDocumentSnapshot.data(as: UserProfile.self)
}
}
}
The id of the UserProfile struct and the id of your data in Firestore are separate. That's why you're seeing the random string of letters.
I would change your struct similar to the below and you can just ignore the regular "id" variable when you initialize and refer to the documentID instead.
struct UserProfile : Codable, Identifiable {
var id = UUID() // ID for the struct
var documentID: String // ID for the post in Database
}
The problem is that you're overwriting the fetched DocumentID with a newly generated UUID string. You should keep it uninitialized, there is no need to set an ID manually. Even if there is, then you should set it only if the object is created from code but not if it gets decoded from Firestore
found my specific problem, I defined CodingKeys enum and when you do that, you have to make sure that you have a mapping for every attribute.
make sure you put all of your model attributes in CodingKeys enum or don't use custom CodingKeys at all
EDIT: taking the above back...it's still buggy...maybe decodes fine but not encodes and uploading
my workaround for now is to not use .adddocument but instead .setdataenter image description here

Why converting a Firestore querySnapshot into custom objects with compactMap returns empty although the querySnapshot contains documents?

Screenshot of a Firestore Document
I am using Swift, Xcode and a Firestore database.
I created a TableView and a Custom Object Class (MediumSample) with a dictionary and want to load my Firestore documents and show them in the TableView.
The documents (example in the screenshot) are loaded from Firestore correctly but the conversion into the object did not work. The list of objects returned from compactMap is always empty.
Here is my code snippets. It would be great, if someone has a hint on what is going wrong.
Custom Object Class (simplified):
import Foundation
import FirebaseFirestore
protocol MediumSampleDocumentSerializable {
init?(dictionary: [String:Any])
}
struct MediumSample {
var id: String
var field_t: String
var field_i: Int64
var field_b1: Bool
var field_b2: Bool
var field_g: FirebaseFirestore.GeoPoint
var field_d: Date
var field_a: [String]
var usecase: String
var dictionary: [String:Any] {
return [
"id": id,
"field_t": field_t,
"field_i": field_i,
"field_b1": field_b1,
"field_b2": field_b2,
"field_g": field_g,
"field_d": field_d,
"field_a": field_a,
"usecase": usecase
]
}
}
extension MediumSample: MediumSampleDocumentSerializable {
init?(dictionary: [String:Any]) {
guard let id = dictionary ["id"] as? String,
let field_t = dictionary ["field_t"] as? String,
let field_i = dictionary ["field_i"] as? Int64,
let field_b1 = dictionary ["field_b1"] as? Bool,
let field_b2 = dictionary ["field_b2"] as? Bool,
let field_g = dictionary ["field_g"] as? FirebaseFirestore.GeoPoint,
let field_d = dictionary ["field_d"] as? Date,
let field_a = dictionary ["field_a"] as? [String],
let usecase = dictionary ["usecase"] as? String else {return nil}
self.init (id: id, field_t: field_t, field_i: field_i, field_b1: field_b1, field_b2: field_b2, field_g: field_g, field_d: field_d, field_a: field_a, usecase: usecase)
}
}
Declaration of the database and array and calling the loading function:
import UIKit
import FirebaseFirestore
class MediumTableViewController: UITableViewController {
//MARK: Properties
var db: Firestore!
var mediumsamples = [MediumSample]()
override func viewDidLoad() {
super.viewDidLoad()
db = Firestore.firestore()
loadMediumSamples()
}
Function for loading the Firestore documents to fill the Array:
private func loadMediumSamples() {
//run the Firestore query
db.collection(Constants.MEDIUMS).whereField("usecase", isEqualTo: Constants.USECASE)
.getDocuments() { querySnapshot, err in
if let err = err {
print("Error getting documents: \(err)")
} else {
//initialise an array of medium objects with Firestore data snapshots
self.mediumsamples = querySnapshot!.documents.compactMap({MediumSample(dictionary: $0.data())})
//fill the tableView
DispatchQueue.main.async {
self.tableView.reloadData()
print(self.mediumsamples)
}
print("Mediums List", self.mediumsamples) // This line returns: Mediums List []
print("Mediums List size", (self.mediumsamples.count)) // This line returns: Mediums List size 0
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())") // This line returns the snapshot documents correctly!
}
}
}
}
Here is how the screenshot object object is added:
func addMediumSamples() {
let currentDateTime = Date()
let location = FirebaseFirestore.GeoPoint(latitude: 0, longitude: 0)
let mediumsample = MediumSample(id: "an id", field_t: "field_t", field_i: 10, field_b1: true, field_b2: false, field_g: location, field_d: currentDateTime, field_a: [Constants.SAMPLE_DEV], usecase: Constants.SAMPLE_DEV)
var ref: DocumentReference? = nil
ref = self.db.collection(Constants.MEDIUMS).addDocument(data: mediumsample.dictionary) {
error in
if let error = error {
print("Error writing city to Firestore: \(error)")
} else {
print("Document added with id : \(ref!.documentID)")
}
}
}
The problem is in the MediumSample struct, in the field_d type (Date).
The type of that field in your Cloud Firestore database is Timestamp.
The field "field_d" in the MediumSample struct expects a value of type Date.
You can change the type to the FirebaseFirestore.Timestamp, or you can convert it to Date when mapping and before passing to the MediumSample.
eg. for converting Timestamp to Date in Swift
let date = timestamp.dateValue()

Swift, Firebase: `setValue` giving error "AnyObject cannot be used with Dictionary Literal"

I am experiencing an error Contextual type AnyObject cannot be used within dictionary literal in that func addPet down there, while trying to populate database with the newPet argument constituents in a dictionary.
import Foundation
import Firebase
struct Pet {
var name:String?
var type:String?
var age:Int?
var feedingList:[String]
var walkingList:[String]
}
struct User {
var currentId:String?
var numberOfPets:Int?
var pets:[Pet]
}
class petBrain {
var reff = FIRDatabaseReference()
var currentUser:User = User(currentId: "",numberOfPets: 0,pets: [])
init(){
self.reff = FIRDatabase.database().reference()
}
func setUserId(cId:String?){
self.currentUser.currentId = cId
}
func addPet(newPet:Pet) {
self.reff.child("pets").childByAutoId().setValue(["name":newPet.name, "type":newPet.type, "age":newPet.age, "fList":newPet.feedingList, "wList":newPet.walkingList])
}
}
I have already done this in other viewController, similarly for users and its working fine in dictionary shape (producing no error)
let em = emailTextField.text!
let us = usernameTextField.text!
...
else {
print("User created! Loging in.")
self.login()
// adding user to DB of users
self.ref.child("users").child(user!.uid).setValue(["email":em, "username":us])
}
What did i do wrong in the pets case? its maybe due to struct, or struct element types? Are those two structs well defined?
? is used if the value can become nil in the future.
! is used if it really shouldn't become nil in the future, but it needs to be nil initially.
See the problem is Swift is a strictly typed language, if you declare a variable ? you are saying that it's of type nil. So a dictionary cant tell what type of a value would it be storing....
var myVar : String? // Look myVar, you and your type is completely nil as of now
var myVar : String! // Look myVar, your value is nil as of now but your type is certainly of String
Just change your code to this:-
struct Pet {
var name:String!
var type:String!
var age:Int!
var feedingList:[String]
var walkingList:[String]
}
struct User {
var currentId:String?
var numberOfPets:Int?
var pets:[Pet]
}
class petBrain {
var reff = FIRDatabaseReference()
var currentUser:User = User(currentId: "",numberOfPets: 0,pets: [])
init(){
self.reff = FIRDatabase.database().reference()
}
func setUserId(cId:String?){
self.currentUser.currentId = cId
}
func addPet(newPet:Pet) {
let dict = ["name":newPet.name, "type":newPet.type, "age":newPet.age, "fList":newPet.feedingList, "wList":newPet.walkingList]
self.reff.child("pets").childByAutoId().setValue(dict)
}
}