How to store field of a Firebase document in swift variable - swift

I'm trying to access an array of references thats a field in a "post" document I have in Firebase and using the values in that array fetch posts that they reference.
However, if I call getDocument() on the document I'm interested in and try to return the array I get the following error. So how can I access the array and use its contents to perform other actions? Thanks!
Cannot convert value of type '[String]' to closure result type 'Void'
let array = usersReference.document(user.id).getDocument() { document, error in
return document["eventsAttending"] as? [String] ?? [""]
}

It is hard to say why this is failing without seeing your data model. When asking questions that relate to Firestore, I would recommend to also post a screenshot of one of your documents from the Firebase console.
That being said, I would recommend using Codable instead of mapping manually, as this will make your life easier.
In your case, this would look something like this:
Model
struct Event: Codable {
#DocumentID var id: String?
var eventsAttending: String?
}
Fetching data
func fetchEvents(documentId: String) {
let docRef = db.collection("events").document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.book = try document.data(as: Event.self)
}
catch {
print(error)
}
}
}
}
}
Check out this blog post to learn more.

Related

Firestore Database structure question & Swift

I have recently started learning Swift and Firestore and i have a question around how to structure a database for what im doing.
At the moment i have the following structure.
enter image description here
What i would like to do is to fetch the newest collection with a specific ownerID in the above structure am i right in suggesting i would have to fetch all documents and add it to an array and go from there, or would it work better with a structure where the document's id is the ownerID. I feel like fetching all the documents is a real waste and would become really problematic if there were lots of documents.
It maybe obvious that im a little lost here.
Thanks in advance
Luke
At the moment i have fetched the documents and sorted them from newest to oldest but now that i need the newest one with a specific ownerID i feel thats its a really clumsy way to do it.
import Foundation
import FirebaseFirestore
import FirebaseFirestoreSwift
class ThingManager: ObservableObject {
#Published private(set) var things: [Things] = []
#Published private(set) var lastThingId = ""
#Published var firstElement = ""
let db = Firestore.firestore()
init(){
getThings()
}
func getThings() {
db.collection("Things").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("Error: fetching documents \(String(describing: error))")
return
}
self.things = documents.compactMap{ document -> Things? in
do {
return try document.data(as: Things.self)
} catch {
print("Error: decoding document into Thing failed: \(error)")
return nil
}
}
self.things.sort { $0.timestamp > $1.timestamp }
if let Element = self.things.first?.thing {
self.firstElement = Element
print("First Element : \(self.firstElement)")
}
if let id = self.things.last?.id {
self.lastThingId = id
}
}
}

How I can get all of users lists from Firestore?

class UsersViewModel : ObservableObject {
#Published var users = [CurrentUser]()
init() {
fetchUserLists()
print(users)
}
func fetchUserLists() {
FirebaseManager.shared.firestore.collection("users")
.getDocuments { documentSnapshot, error in
if let error = error {
print("Error to get user lists")
return
}
//success
documentSnapshot?.documents.forEach({ snapshot in
let user = try? snapshot.data(as: CurrentUser.self)
if user?.uid != FirebaseManager.shared.auth.currentUser?.uid {
self.users.append(user!)
}
})
}
}
}
I'm trying to fetch all of users in my firestore database, but unfortunately my users array is empty. I don't know what my mistake is.
Please check my firestore screen shot, and give me tips!
Thank you!
You're having an issue with asynchronous code. Code is faster than the internet and you have to allow time for data to be retrieved from Firebase.
Additionally, Firebase data is only valid within the closure following the Firebase call. In this case your code is attempting to print an array before it's been filled.
Here's the issue
init() {
fetchUserLists() //<-takes time to complete
print(users) //this is called before fetchUserLists fills the array
}
here's the fetchUserLists function with where the print statement should be
func fetchUserLists() {
FirebaseManager.shared.firestore.collection("users").getDocuments { documentSnapshot, error in
if let error = error {
print("Error to get user lists")
return
}
documentSnapshot?.documents.forEach({ snapshot in
let user = try? snapshot.data(as: CurrentUser.self)
if user?.uid != FirebaseManager.shared.auth.currentUser?.uid {
self.users.append(user!)
}
})
print(self.users) //we are within the closure and the array is now populated
//this is a good spot to, for example, reload a tableview
// or update other UI elements that depend on the array data
}
}

Removing an array item from Firestore not working when array contains date

I've spent days researching this including various answers like: Firebase Firestore: Append/Remove items from document array and my previous question at: Removing an array item from Firestore
but can't work out how to actually get this working. Turns out the issue is when there is a date property in the object as shown below:
I have two structs:
struct TestList : Codable {
var title : String
var color: String
var number: Int
var date: Date
var asDict: [String: Any] {
return ["title" : self.title,
"color" : self.color,
"number" : self.number,
"date" : self.date]
}
}
struct TestGroup: Codable {
var items: [TestList]
}
I am able to add data using FieldValue.arrayUnion:
#objc func addAdditionalArray() {
let testList = TestList(title: "Testing", color: "blue", number: Int.random(in: 1..<999), date: Date())
let docRef = FirestoreReferenceManager.simTest.document("def")
docRef.updateData([
"items" : FieldValue.arrayUnion([["title":testList.title,
"color":testList.color,
"number":testList.number,
"date": testList.date]])
])
}
The above works as reflected in the Firestore dashboard:
But if I try and remove one of the items in the array, it just doesn't work.
#objc func deleteArray() {
let docRef = FirestoreReferenceManager.simTest.document("def")
docRef.getDocument { (document, error) in
do {
let retrievedTestGroup = try document?.data(as: TestGroup.self)
let retrievedTestItem = retrievedTestGroup?.items[1]
guard let itemToRemove = retrievedTestItem else { return }
docRef.updateData([
"items" : FieldValue.arrayRemove([itemToRemove.asDict])
]) { error in
if let error = error {
print("error: \(error)")
} else {
print("successfully deleted")
}
}
} catch {
}
}
}
I have printed the itemToRemove to the log to check that it is correct and it is. But it just doesn't remove it from Firestore. There is no error returned, yet the "successfully deleted" is logged.
I've tried different variations and this code works as long as I don't have a date property in the struct/object. The moment I add a date field, it breaks and stops working. Any ideas on what I'm doing wrong here?
Please note: I've tried passing in the field values as above in FieldValue.arrayUnion as well as the object as per FieldValue.arrayRemove and the same issue persists regardless of which method I use.
The problem is, as you noted, the Date field. And it's a problem because Firestore does not preserve the native Date object when it's stored in the database--they are converted into date objects native to Firestore. And the go-between these two data types is a token system. For example, when you write a date to Firestore from a Swift client, you actually send the database a token which is then redeemed by the server when it arrives which then creates the Firestore date object in the database. Conversely, when you read a date from Firestore on a Swift client, you actually receive a token which is then redeemed by the client which you then can convert into a Swift Date object. Therefore, the definition of "now" is not the same on the client as it is on the server, there is a discrepancy.
That said, in order to remove a specific item from a Firestore array, you must recreate that exact item to give to FieldValue.arrayRemove(), which as you can now imagine is tricky with dates. Unlike Swift, you cannot remove items from Firestore arrays by index. Therefore, if you want to keep your data architecture as is (because there is a workaround I will explain below), the safest way is to get the item itself from the server and pass that into FieldValue.arrayRemove(). You can do this with a regular read and then execute the remove in the completion handler or you can perform it atomically (safer) in a transaction.
let db = Firestore.firestore()
db.runTransaction { (trans, errorPointer) -> Any? in
let doc: DocumentSnapshot
let docRef = db.document("test/def")
// get the document
do {
try doc = trans.getDocument(docRef)
} catch let error as NSError {
errorPointer?.pointee = error
return nil
}
// get the items from the document
if let items = doc.get("items") as? [[String: Any]] {
// find the element to delete
if let toDelete = items.first(where: { (element) -> Bool in
// the predicate for finding the element
if let number = element["number"] as? Int,
number == 385 {
return true
} else {
return false
}
}) {
// element found, remove it
docRef.updateData([
"items": FieldValue.arrayRemove([toDelete])
])
}
} else {
// array itself not found
print("items not found")
}
return nil // you can return things out of transactions but not needed here so return nil
} completion: { (_, error) in
if let error = error {
print(error)
} else {
print("transaction done")
}
}
The workaround I mentioned earlier is to bypass the token system altogether. And the simplest way to do that is to express time as an integer, using the Unix timestamp. This way, the date is stored as an integer in the database which is almost how you'd expect it to be stored anyway. This makes locating array elements that contain dates simpler because time on the client is now equal to time on the server. This is not the case with tokens because the actual date that is stored in the database, for example, is when the token is redeemed and not when it was created.
You can extend Date to conveniently convert dates to timestamps and extend Int to conveniently convert timestamps to dates:
typealias UnixTimestamp = Int
extension Date {
var unixTimestamp: UnixTimestamp {
return UnixTimestamp(self.timeIntervalSince1970 * 1_000) // millisecond precision
}
}
extension UnixTimestamp {
var dateObject: Date {
return Date(timeIntervalSince1970: TimeInterval(self / 1_000)) // must take a millisecond-precision unix timestamp
}
}
One last thing is that in my example, I located the element to delete by its number field (I used your data), which I assumed to be a unique identifier. I don't know the nature of these elements and how they are uniquely identified so consider the filter predicate in my code to be purely an assumption.

(Swift) How to retrieve array -> mapped object data from Firestore Cloud [duplicate]

In Swift, to retrieve an array from Firestore I use:
currentDocument.getDocument { (document, error) in
if let document = document, document.exists {
let people = document.data()!["people"]
print(people!)
} else {
print("Document does not exist")
}
}
And I receive data that looks like this
(
{
name = "Bob";
age = 24;
}
)
However, if I were to retrieve the name alone, normally I'd do print(document.data()!["people"][0]["name"]).
But the response I get is Value of type 'Any' has no subscripts
How do I access the name key inside that object inside the people array?
The value returned by document.data()!["people"] is of type Any and you can't access [0] on Any.
You'll first need to cast the result to an array, and then get the first item. While I'm not a Swift expert, it should be something like this:
let people = document.data()!["people"]! as [Any]
print(people[0])
A better way of writing #Frank van Puffelen's answer would be:
currentDocument.getDocument { document, error in
guard error == nil, let document = document, document.exists, let people = document.get("people") as? [Any] else { return }
print(people)
}
}
The second line may be a little long, but it guards against every error possible.

How can I add these Firestore fields to a Dictionary?

I am looking to add all my "usernames" into a dictionary. I am having some trouble doing this. I am sure it's very obvious, but I am very new to coding.
I am stuck at, right now and can't seem to find a clear answer anywhere:
func fetchUser() {
let db = Firestore.firestore()
let usernameSearch = db.collection("users")
usernameSearch.getDocuments { (snapshot, error) in
if error != nil {
print("Error obtaining usernames")
} else {
for field in snapshot!.documents {
let field = field.get("username")
print(field!)
}
}
}
}
I would really appreciate it if somebody could help me out. I am sure it's very obvious, or I'm just doing it totally wrong.
First, get into the habit of safely unwrapping over force unwrapping. And choose more accurate names for your objects (i.e. usersCollection over usernameSearch). However, in this case, there's no need to instantiate individual properties for the database and the collection since they're not being used anywhere else but here (so be efficient and omit them).
var usersDictionary = [String: [String]]()
func fetchUser() {
Firestore.firestore().collection("users").getDocuments { (snapshot, error) in
if let snapshot = snapshot { // unwrap the snapshot safely
var usernames = [String]()
for doc in snapshot.documents {
if let username = doc.get("username") as? String {
usernames.append(username)
}
}
usersDictionary["usernames"] = usernames
} else {
if let error = error {
print(error)
}
}
}
}
Or if you actually meant an array of users:
var usersArray = [String]()
func fetchUser() {
Firestore.firestore().collection("users").getDocuments { (snapshot, error) in
if let snapshot = snapshot { // don't force unwrap with !
for doc in snapshot.documents {
if let username = doc.get("username") as? String {
usersArray.append(username)
}
}
} else {
if let error = error {
print(error)
}
}
}
}
I'm assuming that what you're looking for is an Array, not a Dictionary. I'll also assume that you are indeed getting the correct value that you'd expect out of field.get("username"), e.g. a string such as "Bob." Therefore, what you are trying to do is map the list of document objects to a list of strings.
If you scroll to the Topics section of the Array documentation from Apple, you can find some of the operations they provide for arrays such as snapshot!.documents.
One of those operations is actually map, and its description is:
Returns an array containing the results of mapping the given closure over the sequence’s elements.
https://developer.apple.com/documentation/swift/array/3017522-map
In other words, you provide a transformation to perform for each instance of a document belonging to the snapshot!.documents Array and get back a new Array containing the resultant values of that transformation.
In this case I will use a more specific operation; compactMap. We have to try and cast the returned value from Any to String. If that does not succeed, it will return nil, and we'll want to filter that out. I expect it to be an unlikely case due to the type requirements made by the Firebase Console, but it's good to be aware of it. Here is the example:
func fetchUsernames(from usernameCollection: String, completion: #escaping ([String]) -> Void) {
let db = Firestore.firestore()
let collection = db.collection(usernameCollection)
collection.getDocuments { snapshot, error in
guard error != nil,
let usernames = snapshot?.documents.compactMap { $0.get("username") as? String }
else { return print("Error obtaining usernames") }
completion(usernames)
}
}
The key line here being let usernames = snapshot?.documents.compactMap { $0.get("username") }. We are passing the map function a closure. This closure is passed an argument itself; each value from the snapshot?.documents array. You may refer to this passed in value with $0.