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

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

Related

delete element from cloud firestore array in swiftui

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

How to Fetch Multiple Types From CloudKit using CKRecord.ID?

When fetching multiple types from CloudKit using CKRecord.ID I get the following error.
Error
Cannot invoke 'map' with an argument list of type '(#escaping (CKRecord.ID, String, CKAsset, Int) -> Lib)'
CloudKit Fetch Function
static func fetch(_ recordID: [CKRecord.ID], completion: #escaping (Result<[Lib], Error>) -> ()) {
let recordID: [CKRecord.ID] = recordID
let operation = CKFetchRecordsOperation(recordIDs: recordID)
operation.qualityOfService = .utility
operation.fetchRecordsCompletionBlock = { (record, err) in
guard let record = record?.values.map(Lib.init) ?? [] //returns error here
else {
if let err = err {
completion(.failure(err))
}
return
}
DispatchQueue.main.async {
completion(.success(record))
}
}
CKContainer.default().publicCloudDatabase.add(operation)
}
Lib
struct Lib {
var recordID: CKRecord.ID
var name: String
var asset: CKAsset
var rating: Int
}
How can I retrieve multiple types from CloudKit using the CKRecord.ID?
You haven't defined an initializer that accepts a CKRecord.
This will make it compile:
extension Lib {
init(_: CKRecord) { fatalError() }
}
Get rid of your ?? [] and go from there!
It may help you if you use accurate pluralization:
operation.fetchRecordsCompletionBlock = { records, error in
guard let libs = records?.values.map(Lib.init)
The answer from #Jessey was very helpful but kept returning a fatalError(). What ended up working was adding to the init.
Lib
struct Lib {
var recordID: CKRecord.ID
var name: String
var asset: CKAsset
var rating: Int
init(record: CKRecord){
recordID = record.recordID
name = (record["name"] as? String)!
asset = (record["asset"] as? CKAsset)!
rating = (record["rating"] as? Int)!
}
}
The "name", "asset", and "rating" are the custom field names in CloudKit dashboard records. I also got rid of the ?? [] in the fetch function per the instruction.
CloudKit Tutorial: Getting Started was a good reference.

Update an existing item in a ChatModel dictionary from a Firestore snapshot

I want to show an overview in a TableView of all existing chats (Name, LastMessage) for a specific user.
Right now, I append a new item in my TableView, which is obviously wrong, instead I want to update the existing item by it's key "ChatId"
This is my Model:
class ChatModel {
var chatOwnerId: String?
var chatPartnerId: String?
var createdDate: Timestamp?
var chatId: String?
var lastMessage: String?
var lastMessageDate: Timestamp?
init(dictionary: [String: Any]) {
chatOwnerId = dictionary["chatOwnerId"] as? String
chatPartnerId = dictionary["chatPartnerId"] as? String
createdDate = dictionary["createdDate"] as? Timestamp
chatId = dictionary["chatId"] as? String
lastMessage = dictionary["lastMessage"] as? String
lastMessageDate = dictionary["lastMessageDate"] as? Timestamp
}
}
How I add the data to my model:
func loadChatOverviewNew(currentUser: String) {
ChatApi.shared.observeChatOverviewNew(currentUser: currentUser) { (chatOverview) in
self.chats.insert(chatOverview, at: 0)
self.chatTableView.reloadData()
}
}
How can I update my "chats" when I receive a new snapshot, instead of appending / inserting it?
To update an existing item, find that item in the array by calling ``, and then update it.
Something like:
ChatApi.shared.observeChatOverviewNew(currentUser: currentUser) { (chatOverview) in
if let index = self.chats.firstIndex(where: { $0.chatId == chatOverview.chatId }) {
self.chats[index] = chatOverview
}
else {
self.chats.insert(chatOverview, at: 0)
}
self.chatTableView.reloadData()
}

Getting an empty array from Firestore although it is not nil

I'm trying to get a String array I have inside a document in my Firestore Database.
I added (Manually) one item to this array, but when I'm trying to get the items in that array, I always get an empty array instead.
This is my code so far:
func getUserFollowingList(id userId: String, completion: #escaping(Array<String>)->()) {
let followingArray = [String]()
db.collection(USERS_COLLECTION).document(userId).getDocument { (doc, error) in
if let err = error {
print(err.localizedDescription)
completion(followingArray)
}
guard let following = doc?.get(USER_FOLLOWING) else { return }
let followingList = following as! [String]
completion(followingList)
}
}
I'm trying to get the firends list of the current user.
This is my Firestore database:
following
0 "9RtM0wgiKee6I5Lo8EugTFihyXE3"
profile_image: "IMAGE_URL"
useremail: "user#test.com"
username: "Testuser"

How do I remove "Optional()" from object in an array

So im using CloudKit and fetching all the data into an array as [StartDay], my StartDay class looks like this:
import UIKit
import CloudKit
class StartDay {
var recordID: CKRecord.ID!
var wakeUp: String!
var sleptWell: String!
var dNN: String!
var created: String! {
get {
return created
}
}
}`
My function loads get an arraylist, which contains information received from the database. In my database it stands like this: "22.01.09:
func checkIfButtonShouldBeEnabled(startDayList: [StartDay]){
let startDayDates = startDayList.map{$0.created}
for i in 0..<startDayDates.count {
print(startDayDates)
}
}`
OUTPUT:
Optional("22.01.2019")
Optional("22.01.2019")
I want to remove "Optional()", so it only says "22.01.2019", how can I do so?
UPDATE: FETCH FUNC
func loadStartDay() -> [StartDay]{
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "StartDay", predicate: predicate)
let operation = CKQueryOperation(query: query)
var startDays: [StartDay] = []
operation.desiredKeys = ["wakeUp", "wellSlept", "dNN", "recordID", "createdDato"]
operation.recordFetchedBlock = { (record:CKRecord) in
let newStartDay = StartDay()
newStartDay.wakeUp = record.object(forKey: "wakeUP") as? String
newStartDay.sleptWell = record.object(forKey: "sleptWell") as? String
newStartDay.dNN = record.object(forKey: "dNN") as? String
newStartDay.recordID = record.object(forKey: "recordID") as? CKRecord.ID
newStartDay.created = record.object(forKey: "createdDato") as? String
print(newStartDay.created)
startDays.append(newStartDay)
}
You can use print(startDayDates!) or print(startDayDates ?? "default value").
But I recommend usage of startDayList.compactMap() instead of startDayList.map()to ensure your array doesn't contain nil values.
You can also do like this:
startDayList
.compactMap { $0.created }
.forEach { print($0) }
As you designed the database model you exactly know which record attributes always exist. Declaring class properties as implicit unwrapped optional as an alibi not to write an initializer is very bad practice.
Assuming every attribute in a record does have a value declare the properties as non-optional and write an initializer.
At least created and recordID are supposed to have always a value!
import UIKit
import CloudKit
class StartDay {
var recordID: CKRecord.ID
var wakeUp: String
var sleptWell: String
var dNN: String
var created: String
init(record : CKRecord) {
// recordID can be retrieved directly
self.recordID = record.recordID
self.wakeUp = record.object(forKey: "wakeUP") as! String
self.sleptWell = record.object(forKey: "sleptWell") as! String
self.dNN = record.object(forKey: "dNN") as! String
self.created = record.object(forKey: "createdDato") as! String
}
}
and create instances with
operation.recordFetchedBlock = { record in
startDays.append(StartDay(record: record))
}
Now the Optional has gone.
print(startDayList.map{ $0.created })