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

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?

Related

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.

Struggling To Query Using getDocuments() in Firestore Swift

This is the first time I am using a Firestore Query and I'm struggling to parse the data. I normally use the same setup when I get documents (which works), but when I attach it to a query it does not work.
I am trying to query the database for the shop most visited, so I can later set it as favourite.
My Code:
func findFavouriteShop(completed: #escaping ([String]) -> Void)
{
// Variables
let dispatch = DispatchGroup()
var dummyDetails = [String]()
// References
let db = Firestore.firestore()
let userID = Auth.auth().currentUser?.uid
let groupCollectionRef = String("visits-" + userID! )
// Query the database for the document with the most counts
dispatch.enter()
db.collectionGroup(groupCollectionRef).order(by: "count", descending: true).limit(to: 1).getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching documents: \(err)")
}
else {
print(snapshot)
guard let snap = snapshot else {return}
for document in snap.documents {
let data = document.data()
// Start Assignments
let shopName = data["shopName"] as? String
let count = data["count"] as? String
// Append the dummy array
dummyDetails.append(shopName!)
dummyDetails.append(count!)
}
dispatch.leave()
}
dispatch.notify(queue: .main, execute: {
print("USER number of documents appended: \(dummyDetails.count)")
completed(dummyDetails)}
)
}
Using Print statements it seems as if the guard statement kicks the function out. The processor does not reach the for-loop to do the assignments. When I print the snapshot it returns an empty array.
I am sure I have used the wrong notation, but I'm just not sure where.
There's a lot to comment on, such as your choice of collection groups over collections (maybe that's what you need), why you limit the results to one document but feel the need to query a collection, the naming of your collections (seems odd), the query to get multiple shops but creating a function that only returns a single shop, using a string for a count property that should probably be an integer, and using a string array to return multiple components of a single shop instead of using a custom type.
That said, I think this should get you in the right direction. I've created a custom type to show you how I'd start this process but there's a lot more work to be done to get this where you need it to be. But this is a good starting point. Also, there was no need for a dispatch group since you weren't doing any additional async work in the document parsing.
class Shop {
let name: String // constant
var count: Int // variable
init(name: String, count: Int) {
self.name = name
self.count = count
}
}
func findFavouriteShops(completion: #escaping (_ shops: [Shop]?) -> Void) {
guard let userID = Auth.auth().currentUser?.uid else {
completion(nil)
return
}
var temp = [Shop]()
Firestore.firestore().collection("visits-\(userID)").order(by: "count", descending: true).limit(to: 1).getDocuments { (snapshot, error) in
guard let snapshot = snapshot else {
if let error = error {
print(error)
}
completion(nil)
return
}
for doc in snapshot.documents {
if let name = doc.get("shopName") as? String,
let count = doc.get("count") as? String {
let shop = Shop(name: name, count: count)
temp.append(Shop)
}
}
completion(temp)
}
}
You can return a Result type in this completion handler but for this example I opted for an optional array of Shop types (just to demonstrate flexibility). If the method returns nil then there was an error, otherwise there are either shops in the array or there aren't. I also don't know if you're looking for a single shop or multiple shops because in some of your code it appeared you wanted one and in other parts of your code it appeared you wanted multiple.
findFavouriteShops { (shops) in
if let shops = shops {
if shops.isEmpty {
print("no error but no shops found")
} else {
print("shops found")
}
} else {
print("error")
}
}

Display Firestore data into UIPicker Swift 4

I am still new in swift development, my problem is, I have Firestore structure as below:
the problem is to display the list of title from firestore into a uipicker, I need to get data into an array like below:
[firsProgramme, secondProgramme, thirdProgramme]
I managed to display all the "title" from firestore in the string, not in the array
below is the code:
func getprogram() {
let db = Firestore.firestore()
db.collection("Programme").getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else
{
for document in querySnapshot!.documents {
let data = document.data()
let program = data["title"] as? String ?? ""
// let agencyId = document.documentID
print(program)
//print("\(document.documentID) => \(document.data())");
}
}
}
}
result print(program) return as below :
firstprogramme
secondprogramme
thirdprogramme
the other part for UIPicker is already being managed well.
Thanks in advance.
In you class create an array variable to hold information about your programs:
var programsArray: [String] = []
When reading data from Firebase, instead of print(program) use programsArray.append(program). Call UIPickerView reload function after you have all data.
DispatchQueue.main.async {
self.your_picker_view.reloadAllComponents()
}
And of course, use this array inside your pickerview datasource methods.

How to Save to a Custom Join Table Core Data (many-to-many) without unique predicate - ManagedObjectID (Swift)?

I will be super thankful for any help. How can I save instances to a join table without a unique identifier as a predicate? Can I use the managed object id to check if the item exists already?
I'm building an app with different exercise plans. Each plan holds many exercise, and an exercise can belong to many plans. I have structured my data model to include a custom join table so that I can query the completion status of an exercise from within one plan.
I'm sourcing my data from a json file and would like to save it to core data. I'm able to correctly save my CoreExercise, and CorePlan tables, however am having difficulty understanding how to save the instance of the object in the intermediate join table, since I'm unsure of what predicate to use.
I've written a class function to check if the instance exists, and to save it if it doesn't.
class CoreExercisePlan: NSManagedObject {
class func coreExercisesForExercisePlan(exerciseInfo: Exercise, planName: String, inManagedObjectContext context: NSManagedObjectContext) -> CoreExercisePlan? {
let request = NSFetchRequest(entityName: "CoreExercisePlan")
request.predicate = NSPredicate() // Search for ObjectID here? / How?
if let exercisePlan = (try? context.executeFetchRequest(request))?.first as? CoreExercisePlan {
print("we have this exercise plan already saved")
return exercisePlan
} else if let exercisePlan = NSEntityDescription.insertNewObjectForEntityForName("CoreExercisePlan", inManagedObjectContext: context) as? CoreExercisePlan {
exercisePlan.status = 0
exercisePlan.progress = 0
print("we are creating new object")
return exercisePlan
}
return nil
}
private func updateDatabaseWithExercisePlans(){
managedObjectContext?.performBlock {
// Array of exercises for each plan:
let coffeePlanExercises = self.coffeeExercises
let subwayPlanExercises = self.subwayExercises
for exercise in coffeePlanExercises {
_ = CoreExercisePlan.coreExercisesForExercisePlan(exercise, planName: "coffee", inManagedObjectContext: self.managedObjectContext!)
}
for exercise in subwayPlanExercises {
_ = CoreExercisePlan.coreExercisesForExercisePlan(exercise, planName: "subway", inManagedObjectContext: self.managedObjectContext!)
}
do {
try self.managedObjectContext?.save()
} catch let error {
print("printing error here: \(error)")
}
}
}
Is there a way to get the objectID of the instance in the join table, and use that as a predicate? Thanks!