Firestore Database structure question & Swift - 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
}
}
}

Related

How to store field of a Firebase document in swift variable

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.

Swift code is reading Firestore DB using addSnapshotListener but no snapshot or error is created

I have some very simple data in Firestore I am trying to read into my iOS app. There is one collection called "matches" with one document containing two fields "id" and "name".
With the code below I am trying to load the Firestore data into my array of matches, but it is not working. I can see in the Firestore usage data that the DB is being read, but no data is being saved to the local variables. Upon execution of this code I expected the matches array to have one object, but it remains empty. When I debug the code line by line, nothing is executing after this line:
collection.addSnapshotListener { (querySnapshot, error) in
Which to me indicates no snapshot or error are being produced, but I don't know why.
Full code:
import Foundation
import Firebase
class ContentModel: ObservableObject {
let db = Firestore.firestore()
// List of matches
#Published var matches = [Match]()
init() {
// Get database matches
getMatches()
}
func getMatches() {
// Specify path
let collection = db.collection("matches")
// Get documents
collection.addSnapshotListener { (querySnapshot, error) in
print("test") // this print statement never executes
if let error = error {
print("Error retrieving collection: \(error)")
}
else {
// Create an array for the matches
var matches = [Match]()
// Loop through the documents returned
for doc in querySnapshot!.documents {
// Create a new Match instance
var m = Match()
// Parse out the values from the document into the Match instance
m.id = doc["id"] as? String ?? UUID().uuidString
m.name = doc["name"] as? String ?? ""
// Add it to our array
matches.append(m)
}
// Assign our matches to the published property
DispatchQueue.main.async {
self.matches = matches
}
}
}
}
The ContentModel is instantiated in the main .swift file for the project as an environment object. Code below:
import SwiftUI
import Firebase
#main
struct AppName: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
MatchTabView().environmentObject(ContentModel())
}
}
}
Found the issue. The view was loading before the data could be read into my array. I'm not sure why the code is not executed when initially debugged, but the data is be read in once I added error handling so the view can load without data.

Reading Firestore Document containing an array of references

Thanks in advance for the help. I'm teaching myself Swift and trying to figure out how to retrieve the following data from Firebase. Here's my Firebase Data Model...
Groups (Collection)
-> GroupName (String)
-> Owner (References to someone in the Players collection)
Players (Collection)
-> PlayerFirstName
-> PlayerLastName
The Swift I've written to retrieve this data is in a ViewModel. getAllGroups is called from onAppear in the View and looks like this...
class Group: Identifiable, ObservableObject {
var id: String = UUID().uuidString
var name: String?
var owner: Player?
}
class GroupViewModel: ObservableObject {
#Published var groups = [Group]()
private var db = Firestore.firestore()
func getAllGroups() {
db.collection("groups").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No groups")
return
}
self.groups = documents.map { (queryDocumentSnapshot) -> Group in
var group = Group()
let data = queryDocumentSnapshot.data()
group.name = data["name"] as? String ?? ""
//
// LIKE --- SHOULD THIS CALL TO GETPLAYER use AWAIT, FOR EXAMPLE?
// WE'RE EXECUTING THE CLOSURE FOR THE FIRST CALL AND ABOUT TO MAKE A SECOND
//
group.owner = self.getPlayer(playerRef: data["owner"] as! DocumentReference)
return group
}
}
}
func getPlayer(playerRef: DocumentReference) -> Player {
var player = Player()
playerRef.getDocument { (document, error) in
guard error == nil else {
print ("error", error ?? "")
return
}
if let document = document, document.exists {
let data = document.data()
if let data = data {
player.firstName = data["firstname"] as? String
player.lastName = data["lastname"] as? String
}
}
}
return player
}
}
The sorta obvious problem here is the closure for retrieving the parent Group executes and then goes and tries to retrieve the Owner. But by the time the closure inside getPlayer completes... the Group has already been established.
Groups will have...
group[0]
-> GroupName = "Cool Name Here"
-> Owner = nil
group[0]
-> GroupName = "Different Cool Name"
-> Owner = nil
even though each Group definitely has an Owner.
I get there's some stuff here about asynchronous calls in Swift and how best to handle that... I'm just not sure what the proper pattern is. Thanks again for the help and advice!
-j
To restate the question:
How do you nest Firestore functions
There are 100 ways to do it and, a lot of it depends on the use case. Some people like DispatchGroups, others like escaping completion handlers but in a nutshell, they pretty much do the "same thing" as the following code, written out "long hand" for readability
func populateGroupArray() {
db.collection("groups").addSnapshotListener { (querySnapshot, error) in
guard let docs = querySnapshot?.documents else { return }
for doc in docs {
let groupName = doc.get("name") as! String
let ownerId = doc.get("owner_id") as! String
self.addToArray(groupName: groupName, andOwnerId: ownerId)
}
}
}
func addToArray(groupName: String, andOwnerId: String) {
db.collection("owners").document(andOwnerId).getDocument(completion: { snapshot, error in
let name = snapshot?.get("owner_name") as! String
let group = Group(groupName: groupName, ownerName: name)
self.groups.append(group)
})
}
In summary; calling populateGroupArray reads in all of the documents from the groups collection from Firestore (adding a listener too). We then iterate over the returned documents to get each group name and the owner id of the group.
Within that iteration, the group name and ownerId are passed to another function that reads in that specific owner via it's ownerId and retrieves the name
Finally, a Group object is instantiated with groupName and owner name being populated. That group is then added to a class var groups array.
Now, if you ask a Firebaser about this method, they will generally recommend not reading large amounts of Firebase data 'in a tight loop'. That being said, this will work very well for many use cases.
In the case you've got a HUGE dataset, you may want to consider denormalizing your data by including the owner name in the group. But again, that would be a rare situation.

Query Based Synced Realm LinkingObjects returning nil

All the solutions I have found online are for local Realms, not synced Realms (I am using Query based sync). How to do it right for a synced Realm?
I have a Shop object and an Item object. Shop has many items. User can browse the items and should see which shop that item belongs to. Pretty simple and straight forward scenario.
In the Shop class I have:
let items = List<Item>()
and in the Item class I have
let shops = LinkingObjects(fromType: Shop.self, property: "items")
var shop: Shop? { return shops.first }
The Realm query is like this:
private var realm : Realm!
private var subscriptionToken : NotificationToken?
private var syncSubscription : SyncSubscription!
...
...
...
let items = realm.objects(Item.self)
syncSubscriptionItem = items.subscribe()
subscriptionTokenItem = syncSubscriptionItem.observe(\.state, options: .initial) { state in
if state == .complete {
let shopName = items[0].shop?.name // Shop is always nil
}
}
I can see shop only if the shop's owner has logged into the app, which means Realm has synced shop information to local Realm. But for users on other devices, shops never get synced. But how to sync shops for all other users via this type of back linking?
Screenshot is attached to clarify what I mean:
I took a minute and wrote some code based on yours. The code in the question was not complete so I added all of the other elements to the code to paint a more complete picture. Theres really no error checking here so make sure you add that. This is working for me and outputs all of the items associated shops
var items: Results<Item>? = nil
var notificationToken: NotificationToken? = nil
var subscriptionToken: NotificationToken? = nil
var subscription: SyncSubscription<Item>!
var realm: Realm!
func setupItems() {
let config = SyncUser.current?.configuration()
self.realm = try! Realm(configuration: config!)
self.items = self.realm.objects(Item.self)
self.subscription = self.items!.subscribe(named: "my-items")
self.subscriptionToken = subscription.observe(\.state, options: .initial) { state in
if state == .complete {
print("subscription: queried data has been synced")
} else {
print("Subscription State: \(state)")
}
}
self.notificationToken = self.items!.observe { changes in
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
print("notification: initial results are populated")
for item in self.items! {
if let shop = item.shop {
print(shop.name)
} else {
print("\(item.itemName) has no shop")
}
}
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed
print("notification: results, inserted, deleteed or modified")
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}

How do I build a custom object that consists of 2 custom objects for an expanding cell?

I am following along this tutorial here for collapsing UITableViewCells and the mechanics are quite straight forward but I am not quite sure how to populate my model arrays from Firestore. He has manually created the data for demo purposes so naturally as a beginner, I am stumbling since instead of that I am making a network call to Firebase.
My data structure is simple. The base collection (which would populate the title of the cell) extracts data from here: db.collection("Insurance_Plans") and contains the following strings:
- Holder name
- Holder contact etc.
And each insurance holder has multiple properties insured and this is the sub-collection i.e. db.collection("Insurance_Plans").document(planId).("Insured_Property") and data model consists of strings such as:
- Property type
- Property address etc.
What I am doing is creating the main struct:
struct cellData {
var opened = Bool()
var plans = [Plan]()
var properties = [Properties]()
}
and in the class itself I declare an instance of it it as:
var tableViewData = [cellData]()
Then I query the insurance meta data (which has its own function) as follows:
db.collection("Insurance_Plans").getDocuments() {
documents, error in
guard let snapshot = documents else {
let error = error
print("Error fetching documents results: \(error!.localizedDescription)")
return
}
let results = snapshot.documents.map { (document) -> Plan in
if let plan = Plan(dictionary: document.data(), id: document.documentID) {
self.plansArray.append(plan)
self.loadPropertyData(planId: document.documentID) // another function where property details are queried
return plan
} else {
fatalError("Unable to initialize type \(Plan.self) with dictionary \(document.data())")
}
}
self.plansArray = results
self.plansDocuments = snapshot.documents
self.plansTableView.reloadData()
}
Then I query the properties in each plan as such:
db.collection("Insurance_Plans").document(planId).collection("Properties").getDocuments() { documents, error in
guard let snapshot = documents else {
let error = error
print("Error fetching documents results: \(error!.localizedDescription)")
return
}
let results = snapshot.documents.map { (document) -> Property in
if let property = Property(dictionary: document.data()) {
return property
} else {
fatalError("Unable to initialize type \(Property.self) with dictionary \(document.data())")
}
}
self.propertiesArray = results
self.propertiesDocuments = snapshot.documents
self.ordersTableView.reloadData()
}
My question then is how do I enter the insurance meta data and the subsequent properties data into a cellData object and where do I do this?