Firestore query multiple documents with for loop - ios

I am trying to query multiple documents using a for-loop.
My database set up looks like this:
users -> wishlists -> all the users Wishlists(containing different Wishlists with name) -> wünsche
The items are getting retrieved but in the wrong order. I tried couple of different things but nothing worked so far.
func getWishes() {
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
var counter = 0
for list in self.dataSourceArray {
print(list.name) // -> right order
}
for list in self.dataSourceArray {
db.collection("users").document(userID).collection("wishlists").document(list.name).collection("wünsche").getDocuments() { ( querySnapshot, error) in
print(list.name) // -> wrong order
if let error = error {
print(error.localizedDescription)
}else{
// create new Wish array
var wList: [Wish] = [Wish]()
for document in querySnapshot!.documents {
let documentData = document.data()
let wishName = documentData["name"]
wList.append(Wish(withWishName: wishName as! String, checked: false))
}
self.dataSourceArray[counter].wishData = wList
counter += 1
}
}
}
}
I am calling this function inside another function that retrieves all the wishlist in the right order:
func getWishlists() {
let db = Firestore.firestore()
let userID = Auth.auth().currentUser!.uid
db.collection("users").document(userID).collection("wishlists").order(by: "listIDX").getDocuments() { ( querySnapshot, error) in
if let error = error {
print(error.localizedDescription)
}else {
// get all documents from "wishlists"-collection and save attributes
for document in querySnapshot!.documents {
let documentData = document.data()
let listName = documentData["name"]
let listImageIDX = documentData["imageIDX"]
// if-case for Main Wishlist
if listImageIDX as? Int == nil {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: UIImage(named: "iconRoundedImage")!, wishData: [Wish]()))
// set the drop down menu's options
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(UIImage(named: "iconRoundedImage")!)
}else {
self.dataSourceArray.append(Wishlist(name: listName as! String, image: self.images[listImageIDX as! Int], wishData: [Wish]()))
self.dropDownButton.dropView.dropDownOptions.append(listName as! String)
self.dropDownButton.dropView.dropDownListImages.append(self.images[listImageIDX as! Int])
}
// reload collectionView and tableView
self.theCollectionView.reloadData()
self.dropDownButton.dropView.tableView.reloadData()
}
}
self.theCollectionView.isHidden = false
self.getWishes()
}
}
*DataSourceArray in the right order: * Main Wishlist, Goals, boost
Output from 2nd print-test: boost, Goals, Main Wishlist

Seems as though you are trying to make a bunch of API calls at once and it is returning values at different times. You could attempt to make your calls synchronously to maintain order or you could try to use dispatch groups like the pseudo code below:
let myGroup = DispatchGroup()
struct DataItem {
let order: Int
let data: DataYouWantToSave
}
var fetchedData = [DataItem]()
for i in list {
myGroup.enter()
let dataItem = DataItem()
dataItem.order = i
db.collection...
print("Finished request \(i)")
dataItem.data = DataYouWantToSave
fetchedData.apped(dataItem)
myGroup.leave()
}
}
myGroup.notify(queue: .main) {
print("Finished all requests.")
// Reorder your array of data items here.
let sortedArray = fetchedData.sorted(by: { $0.order > $1.order })
// If you just want the array of data values
let newData: [DataYouWantToSave] = sortedArray.map { $0.data }
}

Related

Can't retrieve data from Firestore with addSnapshotListener Swift

I have two ViewControllers. One is for writing data to Firestore and the other is for loading. Now I can successfully write data, but I can't load it. There isn't any error or data showing up. It seems that the addSnapshotListener method was not being called when I tried to observe it with breakpoint. This is my code related with Firestore:
Updated on July 11
I tried to print something to check if the code was actually executed, and I got "inside closure" and "Found document." in my console. It seems that it couldn't find orderDetail in my Firestore document. However, I indeed have orderDetail in each document. Why did this happen?
let database = Firestore.firestore()
struct FStore {
static let collectionName = "orders"
static let foodID = "foodID"
static let orderDetail = "orderDetail"
static let totalPrice = "totalPrice"
static let timeStamp = "created"
}
func loadOrder() {
database.collection(FStore.collectionName).order(by: FStore.timeStamp).limit(to: 1).addSnapshotListener { (snapShot, error) in
print("inside closure")
self.foodList = []
if let e = error {
print("There was an issue retrieving data from Firestore. \(e)")
}
if let snapShotDocuments = snapShot?.documents,
let doc = snapShotDocuments.first {
print("Found document.")
let data = doc.data()
if let orderDetail = data[FStore.orderDetail] as? [String: Int] {
print("Found order detail.")
// Convert to FoodItem and Append back to foodList
for food in orderDetail {
let name = food.key
let price = food.value
let serving = food.value
let newFood = FoodItem(name: name, price: price, serving: serving)
self.foodList.append(newFood)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
print("Successfully loaded data from Firestore.")
}
}
}
}
This is the screenshot of my Firestore:

how to delete UITableView Cell after document gets removed from Firestore console

since of starting my project now i didn't find any answer about how to delete the UITableView cell from an outside source, I'm struggling here about how to delete UITableview Rows or Cells after it gets deleted from My FireStore Cloud Storage, I want to remove the value from the array and delete that cell without using canEditRowAt indexPath cause I want to delete it in
one more thing I don't know how to use dictionaries with arrays, and I don't know if I have to use it in such a case
struct accepteddriverData {
var driverName: String
var userPhone: String
var username: String
var driverPhone: String
}
func driverSelected(arg: Bool, completion: (Bool) -> ()){
guard let driverid = Auth.auth().currentUser?.uid else {return}
let firestore = Firestore.firestore()
let doc = firestore.collection("offers").whereField("driverUid", isEqualTo: driverid).whereField("selected", isEqualTo: true)
doc.addSnapshotListener { (querysnap, err) in
if err != nil {
print(err?.localizedDescription ?? "")
}
querysnap?.documentChanges.forEach({ (change) in
self.DriverData = []
if (change.type == .added) {
let snap = change.document.data()
let driverName = snap["driverName"] as? String ?? ""
let userPhone = snap["userPhone"] as? String ?? ""
let username = snap["userName"] as? String ?? ""
let driverPhone = snap["driverPhone"] as? String ?? ""
let accepted = accepteddriverData(driverName: driverName, userPhone: userPhone, username: username, driverPhone: driverPhone)
self.DriverData.append(accepted)
DispatchQueue.main.async {
self.DriverOrdersTV.reloadData()
}
}
if (change.type == .removed){ // i want to delete tableView cell on this statement
let id = change.document.documentID
let snap = change.document.data()
print(id)
print(snap)
}
})
}
completion(arg)
}
Add to your model
var docId: String
then inside
if (change.type == .removed){
let id = change.document.documentID
self.DriverData.removeAll(where:{ $0.docId == id })
// refresh table
}

Database fetch exists only in its own function

I am trying to fetch all the user names in my database but the nameArray only contains values while its inside that function, how can I fix this?
DataService.instance.getAllUserNamesPlease { (returnedNamesArray) in
self.nameArray = returnedNamesArray
}
for userName in nameArray {
if(userName.lowercased() == name!.lowercased()){
self.userNameTaken = true
self.progressView.progress = Float(progress / self.nameArray.count)
progress += 1/self.nameArray.count
break
}
}
nameArray is empty in this loop
func getAllUserNamesPlease(handler: #escaping (_ userNames: [String]) -> ()){
REF_USERS.observeSingleEvent(of: .value) { (userNameSnapshot) in
guard let userNameSnapshot = userNameSnapshot.children.allObjects as? [DataSnapshot] else {return}
var namesArray = [String]()
for names in userNameSnapshot {
let name = names.childSnapshot(forPath: "userName").value as? String ?? "No Name"
namesArray.append(name)
}
handler(namesArray)
}
}
Any code that needs access to the results of an asynchronous call, should be inside that callback/completion handler. So your loop over nameArray, needs to be inside the {} braces:
DataService.instance.getAllUserNamesPlease { (returnedNamesArray) in
self.nameArray = returnedNamesArray
for userName in nameArray {
if(userName.lowercased() == name!.lowercased()){
self.userNameTaken = true
self.progressView.progress = Float(progress / self.nameArray.count)
progress += 1/self.nameArray.count
break
}
}
}
}

Retrieve data from "getDocument" query inside another "getDocument" query

I have a problem in my code retrieving data from Firestore.
I have 2 classes in my code: Exercise and Tag.
And I have 2 collections in my FirestoreDatabase: exercises and tags
I need to fecth all the "exercise" documents from the "exercises" collection. Every "exercise" document has a field called "tags" that is an array of strings. Each string of the array contains the "id" that refers to the document that "tag" has on "tags" collection. So, querying this id in "tags" collection, enables me to get the correct "tag" document and access all its data. And this is exactly what I want to do in my code.
I need to fecth all the exercises into a Exercise object and for that I have to use a getDocument query inside another getDocument query in order to get "Tags" of the exercise from "tags" collection
This are my classes Tag and Exercise:
class Tag {
var id: String?
var type: String?
var description: String?
init(id: String, type: String, description: String) {
self.id = id
self.type = type
self.description = description
}
}
class Exercise {
let id: String?
let group: String?
let tags: [Tag]
let title : String!
init(id: String, group: String, tags: [Tag], title: String){
self.id = id
self.group = group
self.tags = tags
self.title = title
}
}
And this is the code where I fetch my "exercises" from Firestore database:
func fetchExercises(completion: #escaping ([Exercise]) -> ()) {
let exercisesRef = Firestore.firestore().collection("exercises")
exercisesRef.getDocuments() { (querySnapshot, err) in
var exercisesArray = [Exercise]()
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
//print("\(document.documentID) => \(document.data())")
let myData = document.data()
let exercise_ID = document.documentID
let exercise_group = myData["Group"] as! String
let tagsArray = myData["Tags"] as! [String]
var exercise_tags: [Tag] = [Tag]()
for tag in tagsArray {
let tagID: String = tag
fetchTagfromID(tagID: tagID) { (tag: Tag) in
exercise_tags.append(tag)
}
}
let exercise_title = myData["Title"] as! String
exercisesArray.append(Exercise(id: exercise_ID,
group: exercise_group,
tags: exercise_tags,
title: exercise_title,
))
}
DispatchQueue.main.async{
print("EXERCISE FETCH HAS FINIS")
completion(exercisesArray)
}
}
}
}
func fetchTagfromID(tagID: String, completion: #escaping (Tag) -> ()) {
let tagRef = Firestore.firestore().collection("tags").document(tagID)
tagRef.getDocument() { (document, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
let myData = document?.data()
let tagDescription: String = myData!["description"] as! String
let tagType: String = myData!["type"] as! String
let tag: Tag = Tag(id: tagID, type: tagType, description:
tagDescription)
DispatchQueue.main.async{
print("TAGS FETCH HAS FINISHED")
completion(tag)
}
}
}
}
My problems come with the timing (queues) code is executed.
I need to fill "exercise_tags" first (secondary getDocument query) and then continue and finish fetchExercise (main getDocument query) but Firestore do not allow (or do not know how) to do that. Code finishes first the main getDocument query (fetChExercises) and then get back to finish secondary getDocument Query (fetchTagfromID).
In summary I need that in runtime I get this log:
TAGS FETCH HAS FINISHED
EXERCISES FETCH HAS FINISHED
and now I am getting the opposite.
Do you know guys how to solve this problem? Maybe changing dispatch queing...
I now how to solve the problem doing this in two steps but elegant solution is doing everything in one step. This is, fetchExercises.
Thank you!
So what you need to do is get all tags for one Exercise then fetch another Exercise. and after all fetch call completion handler to update UI. I made some change in code you can check it.
var exercisesArray = [Exercise]()
var listFromFetchExercise = []() //it will contain all object of array
querySnapshot!.documents. set DataType according to that.
var completion_exercises_Listner: () -> ()
func fetchExercises(completion: #escaping ([Exercise]) -> ()) {
let exercisesRef = Firestore.firestore().collection("exercises")
exercisesRef.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
listFromFetchExercise = querySnapshot!.documents
fetchAllExercise()
DispatchQueue.main.async{
print("EXERCISE FETCH HAS FINIS")
completion(exercisesArray)
}
}
}
}
func fetchAllExercise(completion: #escaping ([Exercise]) -> ()){
fetchExercise()
completion_exercises_Listner = {
completion(exercisesArray);
}
}
func fetchExercise(index:Int = 0) {
let document = listFromFetchExercise[index]
let myData = document.data()
let exercise_ID = document.documentID
let exercise_group = myData["Group"] as! String
let tagsArray = myData["Tags"] as! [String]
var exercise_tags: [Tag] = [Tag]()
fetchTagsFromIDS(tagsArray) { (tag: [Tag]) in
exercise_tags.append(contentsOf: tag)
let exercise_title = myData["Title"] as! String
exercisesArray.append(Exercise(id: exercise_ID,
group: exercise_group,
tags: exercise_tags,
title: exercise_title,
DispatchQueue.main.async{
exercisesArray.append(tag)
if (index + 1) < exercisesArray.count {
fetchExercise(index+1,)
}else{
//Done
completion_exercises_Listner()
}
}
))
}
var listofTags = [String]()
var resultofTags = [Tag]()
func fetchTagsFromIDS(tagIDS:[String],completion: #escaping (_ tags:[
String]) -> ()){
listofTags = tagIDS;
fetchTagfromID() //Start With first tag
completionListner = {
completion(resultofTags);
}
}
var completionListner: () -> ()
func fetchTagfromID(index:Int = 0) {
let tagRef = Firestore.firestore().collection("tags").document(tagID)
tagRef.getDocument() { (document, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
let myData = document?.data()
let tagDescription: String = myData!["description"] as! String
let tagType: String = myData!["type"] as! String
let tag: Tag = Tag(id: tagID, type: tagType, description:
tagDescription)
DispatchQueue.main.async{
print("TAGS FETCH HAS FINISHED")
resultofTags.append(tag)
if (index + 1) < listofTags.count {
fetchTagfromID(index+1,)
}else{
//Done
completionListner()
}
//completion(tag)
}
}
} }

Loading 3 Different Information to 3 Different types of Cells

I have three different types of cells in a tableViewController. I get which type of cell to have and an objectId of an item from a different class. I then go to each cell in the cellForRowAt method and load whatever the data is. This method has led me to 2 problems: 1) the dynamic height of one of the cell does not work because it's label's text is not found until after the cell is made. 2) All the cells have a "jumpy" (I can see the rows being populated as I scroll down, I guess because its loading the content every scroll) look as I scroll down the tableview.
So I want to preload all of the data before and put it in the cellForRowAt instead of searching for the data in cellForRowAt. This will fix both problems, but I have no idea how to do this. Based on my coding knowledge I would place the information that would go in each cell in arrays then populate the cells accordingly, but I do not know how to do this when using 3 different cells because to put the information in the cells from the array I would need to use indexPath.row; which I can not do this because I am loading 3 different types of data and adding them to different arrays so the indexPaths will not be aligned properly. This is the only way I can think of doing this and it's wrong. How can I fix this problem?
I have copied my code at the bottom so you can see what how I am loading the cells now and maybe you can get an understanding of how to fix my issue:
func loadNews() {
//start finding followers
let followQuery = PFQuery(className: "Follow")
followQuery.whereKey("follower", equalTo: PFUser.current()?.objectId! ?? String())
followQuery.findObjectsInBackground { (objects, error) in
if error == nil {
//clean followArray
self.followArray.removeAll(keepingCapacity: false)
//find users we are following
for object in objects!{
self.followArray.append(object.object(forKey: "following") as! String)
}
self.followArray.append(PFUser.current()?.objectId! ?? String()) //so we can see our own post
//getting related news post
let newsQuery = PFQuery(className: "News")
newsQuery.whereKey("user", containedIn: self.followArray) //find this info from who we're following
newsQuery.limit = 30
newsQuery.addDescendingOrder("createdAt") //get most recent
newsQuery.findObjectsInBackground(block: { (objects, error) in
if error == nil {
//clean up
self.newsTypeArray.removeAll(keepingCapacity: false)
self.objectIdArray.removeAll(keepingCapacity: false)
self.newsDateArray.removeAll(keepingCapacity: false)
for object in objects! {
self.newsTypeArray.append(object.value(forKey: "type") as! String) //get what type (animal / human / elements)
self.objectIdArray.append(object.value(forKey: "id") as! String) //get the object ID that corresponds to different class with its info
self.newsDateArray.append(object.createdAt) //get when posted
}
self.tableView.reloadData()
} else {
print(error?.localizedDescription ?? String())
}
})
} else {
print(error?.localizedDescription ?? String())
}
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let type = newsTypeArray[indexPath.row]
if type == "element" {
let cell = tableView.dequeueReusableCell(withIdentifier: "ElementCell") as! ElementCell
let query = query(className: "Element")
query.whereKey("objectId", equalTo: self.objectIdArray[indexPath.row])
query.limit = 1
query.findObjectsInBackground(block: { (objects, error) in
if error == nil {
for object in objects! {
let name = (object.object(forKey: "type") as! String)
let caption = (object.object(forKey: "caption") as! String) //small description (usually 2 lines)
cell.captionLabel.text = caption
}
} else {
print(error?.localizedDescription ?? String())
}
})
return cell
} else if type == "human" {
let cell = tableView.dequeueReusableCell(withIdentifier: "HumanCell") as! HumanCell
let query = query(className: "Human")
query.whereKey("objectId", equalTo: self.objectIdArray[indexPath.row])
query.limit = 1
query.findObjectsInBackground(block: { (objects, error) in
if error == nil {
for object in objects! {
let name = (object.object(forKey: "name") as! String)
let caption = (object.object(forKey: "caption") as! String) //small description (1 line)
cell.captionLabel.text = caption
}
} else {
print(error?.localizedDescription ?? String())
}
})
return cell
} else { //its an animal cell
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! AnimalCell
let query = query(className: "Animals")
query.whereKey("objectId", equalTo: self.objectIdArray[indexPath.row])
query.limit = 1
query.findObjectsInBackground(block: { (objects, error) in
if error == nil {
for object in objects! {
let caption = (object.object(forKey: "caption") as! String) //large description of animal (can be 2 - 8 lines)
cell.captionLabel.text = caption
}
} else {
print(error?.localizedDescription ?? String())
}
})
return cell
}
}
----- Edit ------
Implementation of #Woof's Logic:
In separate swift file:
class QueryObject {
var id: String?
var date: Date?
var userID : String?
var name: String?
}
class Element: QueryObject {
var objectID : String?
var owner : String?
var type : String?
var ability : String?
var strength : String?
}
class Human: QueryObject {
var objectID : String?
var follower : String?
var leader : String?
}
class Animal: QueryObject {
var objectID : String?
var type: String?
var owner : String?
var strength : String?
var speed : String?
var durability : String?
}
In TableviewController:
var tableObjects: [QueryObject] = []
func loadNews() {
//start finding followers
let followQuery = PFQuery(className: "Follow")
followQuery.whereKey("follower", equalTo: PFUser.current()?.objectId! ?? String())
followQuery.findObjectsInBackground { [weak self](objects, error) in
if error == nil {
//clean followArray
self?.followArray.removeAll(keepingCapacity: false)
//find users we are following
for object in objects!{
self?.followArray.append(object.object(forKey: "following") as! String)
}
self?.followArray.append(PFUser.current()?.objectId! ?? String()) //so we can see our own post
//this is a custom additional method to make a query
self?.queryNews(name: "News", followArray: self?.followArray ?? [], completionHandler: { (results) in
//if this block is called in a background queue, then we need to return to the main one before making an update
DispatchQueue.main.async {
//check that array is not nil
if let objects = results {
self?.tableObjects = objects
self?.tableView.reloadData()
}else{
//objects are nil
//do nothing or any additional stuff
}
}
})
} else {
print(error?.localizedDescription ?? String())
}
}
}
//I've made the code separated, to make it easy to read
private func queryNews(name: String, followArray: [String], completionHandler: #escaping (_ results: [QueryObject]?) -> Void) {
//making temp array
var temporaryArray: [QueryObject] = []
//getting related news post
let newsQuery = PFQuery(className: "News")
newsQuery.whereKey("user", containedIn: followArray) //find this info from who we're following
newsQuery.limit = 30
newsQuery.addDescendingOrder("createdAt") //get most recent
newsQuery.findObjectsInBackground(block: { [weak self] (objects, error) in
if error == nil {
//now the important thing
//we need to create a dispatch group to make it possible to load all additional data before updating the table
//NOTE! if your data are large, maybe you need to show some kind of activity indicator, otherwise user won't understand what is going on with the table
let dispathGroup = DispatchGroup()
for object in objects! {
//detecting the type of the object
guard let type = object.value(forKey: "type") as? String else{
//wrong value or type, so don't check other fields of that object and start to check the next one
continue
}
let userID = object.value(forKey: "user") as? String
let id = object.value(forKey: "id") as? String
let date = object.createdAt
//so now we can check the type and create objects
//and we are entering to our group now
dispathGroup.enter()
switch type {
case "element":
//now we will make a query for that type
self?.queryElementClass(name: "element", id: id!, completionHandler: { (name, objectID, owner, type, ability, strength) in
//I've added a check for those parameters, and if they are nil, I won't add that objects to the table
//but you can change it as you wish
if let objectName = name, let objectsID = objectID {
//now we can create an object
let newElement = Element()
newElement.userID = userID
newElement.id = id
newElement.date = date
newElement.objectID = objectID
newElement.owner = owner
newElement.type = type
newElement.ability = ability
newElement.strength = strength
temporaryArray.append(newElement)
}
//don't forget to leave the dispatchgroup
dispathGroup.leave()
})
case "human":
//same for Human
self?.queryHumanClass(name: "human", id: id!, completionHandler: { (name, objectID, follower, leader) in
if let objectName = name, let objectsID = objectID {
let newHuman = Human()
newHuman.userID = userID
newHuman.id = id
newHuman.date = date
temporaryArray.append(newHuman)
}
//don't forget to leave the dispatchgroup
dispathGroup.leave()
})
case "animal":
//same for animal
self?.queryAnimalClass(name: "animal", id: id!, completionHandler: { (name, objectID, type, owner, strength, speed, durability) in
if let objectName = name, let objectCaption = caption {
let newAnimal = Animal()
newAnimal.userID = userID
newAnimal.id = id
newAnimal.date = date
temporaryArray.append(newAnimal)
}
//don't forget to leave the dispatchgroup
dispathGroup.leave()
})
default:
//unrecognized type
//don't forget to leave the dispatchgroup
dispathGroup.leave()
}
}
//we need to wait for all tasks entered the group
//you can also add a timeout here, like: user should wait for 5 seconds maximum, if all queries in group will not finished somehow
dispathGroup.wait()
//so we finished all queries, and we can return finished array
completionHandler(temporaryArray)
} else {
print(error?.localizedDescription ?? String())
//we got an error, so we will return nil
completionHandler(nil)
}
})
}
//the method for making query of an additional class
private func queryElementClass(name: String, id: String, completionHandler: #escaping (_ name: String?, _ objectID: String?, _ owner: String?, _ type: String?, _ ability: String?, _ strength: String?) -> Void) {
let query = PFQuery(className: "Elements")
query.whereKey("objectId", equalTo: id)
query.limit = 1
query.findObjectsInBackground { (objects, error) in
if error == nil {
if let object = objects?.first {
let name = object.object(forKey: "type") as? String
let objectID = object.object(forKey: "objectID") as? String
let owner = object.object(forKey: "owner") as? String
let type = object.object(forKey: "type") as? String
let ability = object.object(forKey: "ability") as? String
let strength = object.object(forKey: "strength") as? String
completionHandler(name, objectID, owner, type, ability, strength)
} else {
print(error?.localizedDescription ?? String())
completionHandler(nil, nil, nil, nil, nil, nil)
}
} else {
print(error?.localizedDescription ?? String())
}
}
}
//the method for making query of an additional class
private func queryHumanClass(name: String, id: String, completionHandler: #escaping (_ name: String?, _ objectID: String?, _ follower: String?, _ leader: String?) -> Void) {
let query = PFQuery(className: "Human")
query.whereKey("objectId", equalTo: id)
query.limit = 1
query.findObjectsInBackground(block: { (objects, error) in
if let object = objects?.first {
let name = object.object(forKey: "type") as? String
let objectID = object.object(forKey: "objectID") as? String
let follower = object.object(forKey: "follower") as? String
let leader = object.object(forKey: "leader") as? String
completionHandler(name, objectID, follower, leader)
} else {
print(error?.localizedDescription ?? String())
completionHandler(nil, nil, nil, nil)
}
})
}
//the method for making query of an additional class
private func queryAnimalClass(name: String, id: String, completionHandler: #escaping (_ name: String?, _ objectID: String?, _ owner: String?, _ type: String?, _ strength: String?, _ speed: String?, _ durability: String?) -> Void) {
let query = PFQuery(className: "Animals")
query.whereKey("objectId", equalTo: id)
query.limit = 1
query.findObjectsInBackground(block: { (objects, error) in
if let object = objects?.first {
let name = object.object(forKey: "type") as? String
let objectID = object.object(forKey: "objectID") as? String
let owner = object.object(forKey: "owner") as? String
let strength = object.object(forKey: "strength") as? String
let type = object.object(forKey: "type") as? String
let speed = object.object(forKey: "speed") as? String
let durability = object.object(forKey: "durability") as? String
completionHandler(name, objectID, owner, type, strength, speed, durability)
} else {
print(error?.localizedDescription ?? String())
completionHandler(nil, nil, nil, nil, nil, nil, nil)
}
})
}
Looking at your projects I see multiple arrays with different data. It is very hard to edit your code with this kind of structure.
I would make it in this way:
1) create objects to store values, like structs/classes Animal, Human, Element. If they have same values like ids or whatever, you can create a super class Object and make other objects as subclasses
2) create one array as a data source for your table with objects not values
//if there is no super class
var objects:[AnyObject] = []
Or
//for the superclass
var objects:[YourSuperClass] = []
In the code below I will use Superclass, but you can change it to AnyObject
3) make a method to fill this array of objects before updating the table:
//I think it is better to use clousures and make data fetching in different queue
func loadNews(completionHandler: #escaping (_ objects: [YourSuperClass]) -> Void){
yourBackgroundQueue.async{
var objects = // fill here the array with objects
// it is important to return data in the main thread to make an update
DispatchQueue.main.async{
completion(objects)
}
}
}
And to fill our datasourse array, call this method when you need:
func updateTable(){
loadNews(){ [weak self] objects in
self?.objects = objects
self?.tablewView.reloadData()
}
So now you have an array of objects
4)We can use downcast to the specific class to set cells:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let object = objects[indexPath.row]
//making downcast
if let animal = object as? Animal,
let cell = tableView.dequeueReusableCell(withIdentifier: "AnimalCell") as? AnimalCell
//now you can fill the cell by properties than Animal object has
//return cell
return cell
}
if let human = object as? Human,
let cell = tableView.dequeueReusableCell(withIdentifier: "HumanCell") as? HumanCell
//do stuff with HumanCell
//return cell
return cell
}
//same way you can detect and fill any other cells
//this will be return an empty cell if there will be an object in the array that wasn't recognized. In this case the app won't crash, but you will see that something is wrong
return UITableViewCell()
}
So main thoughts:
make full loading before updating in separated queue (there may be exceptions, like if you would have to load images and don't what to wait all images to be downloaded before showing the table, it is better to fill the cells using simple values and then make an image loading inside each cell and show some activity indicator for each one)
create an array of objects with parameters, instead of making several arrays with simple values
use an array of objects to determine a cell type in the table.
============EDIT================
NOTE! I've made that code in the playground without importing PFQuery
If there will be errors, let me know. If you will stuck, let me know, maybe I will check your project directly
So, new code
//declaring Objects in separated file
class QueryObject {
var id: String?
var date: Date? //change of your date for object.createdAt has different type
var caption: String?
var name: String?
// var type: String? //use this var you don't need to have subclasses
}
//If your subclasses will not have unique parameters, you can left only one class QueryObject, without subclasses
//In this case just uncomment the "type" variable in the QueryObject, then you can check that var in cellForRowAt
class Animal: QueryObject {
//add any additional properties
}
class Human: QueryObject {
//add any additional properties
}
class Element: QueryObject {
//add any additional properties
}
class YourController: UITableViewController {
//allocate var inside ViewController
var tableObjects: [QueryObject] = []
func loadNews() {
//start finding followers
let followQuery = PFQuery(className: "Follow")
followQuery.whereKey("follower", equalTo: PFUser.current()?.objectId! ?? String())
followQuery.findObjectsInBackground { [weak self](objects, error) in
if error == nil {
//clean followArray
self?.followArray.removeAll(keepingCapacity: false)
//find users we are following
for object in objects!{
self?.followArray.append(object.object(forKey: "following") as! String)
}
self?.followArray.append(PFUser.current()?.objectId! ?? String()) //so we can see our own post
//this is a custom additional method to make a query
self?.queryNews(name: "News", followArray: self?.followArray ?? [], completionHandler: { (results) in
//if this block is called in a background queue, then we need to return to the main one before making an update
DispatchQueue.main.async {
//check that array is not nil
if let objects = results {
self?.tableObjects = objects
self?.tableView.reloadData()
}else{
//objects are nil
//do nothing or any additional stuff
}
}
})
} else {
print(error?.localizedDescription ?? String())
}
}
}
//I've made the code separated, to make it easy to read
private func queryNews(name: String, followArray: [String], completionHandler: #escaping (_ results: [QueryObject]?) -> Void) {
//making temp array
var temporaryArray: [QueryObject] = []
//getting related news post
let newsQuery = PFQuery(className: "News")
newsQuery.whereKey("user", containedIn: followArray) //find this info from who we're following
newsQuery.limit = 30
newsQuery.addDescendingOrder("createdAt") //get most recent
newsQuery.findObjectsInBackground(block: { [weak self] (objects, error) in
if error == nil {
//now the important thing
//we need to create a dispatch group to make it possible to load all additional data before updating the table
//NOTE! if your data are large, maybe you need to show some kind of activity indicator, otherwise user won't understand what is going on with the table
let dispathGroup = DispatchGroup()
for object in objects! {
//detecting the type of the object
guard let type = object.value(forKey: "type") as? String else{
//wrong value or type, so don't check other fields of that object and start to check the next one
continue
}
let id = object.value(forKey: "id") as? String
let date = object.createdAt
//so now we can check the type and create objects
//and we are entering to our group now
dispathGroup.enter()
switch type {
case "animal":
//now we will make a query for that type
self?.queryAdditionalClass(name: "Animals", id: id, completionHandler: { (name, caption) in
//I've added a check for those parameters, and if they are nil, I won't add that objects to the table
//but you can change it as you wish
if let objectName = name, let objectCaption = caption {
//now we can create an object
let newAnimal = Animal()
newAnimal.id = id
newAnimal.date = date
temporaryArray.append(newAnimal)
}
//don't forget to leave the dispatchgroup
dispathGroup.leave()
})
case "human":
//same for Human
self?.queryAdditionalClass(name: "Human", id: id, completionHandler: { (name, caption) in
if let objectName = name, let objectCaption = caption {
let newHuman = Human()
newHuman.id = id
newHuman.date = date
temporaryArray.append(newHuman)
}
//don't forget to leave the dispatchgroup
dispathGroup.leave()
})
case "elements":
//same for Element
self?.queryAdditionalClass(name: "Element", id: id, completionHandler: { (name, caption) in
if let objectName = name, let objectCaption = caption {
let newElement = Element()
newElement.id = id
newElement.date = date
temporaryArray.append(newElement)
}
//don't forget to leave the dispatchgroup
dispathGroup.leave()
})
default:
//unrecognized type
//don't forget to leave the dispatchgroup
dispathGroup.leave()
}
}
//we need to wait for all tasks entered the group
//you can also add a timeout here, like: user should wait for 5 seconds maximum, if all queries in group will not finished somehow
dispathGroup.wait()
//so we finished all queries, and we can return finished array
completionHandler(temporaryArray)
} else {
print(error?.localizedDescription ?? String())
//we got an error, so we will return nil
completionHandler(nil)
}
})
}
//the method for making query of an additional class
private func queryAdditionalClass(name: String, id: String, completionHandler: #escaping (_ name: String?, _ caption: String?) -> Void) {
let query = PFQuery(className: name)
query.whereKey("objectId", equalTo: id)
query.limit = 1
query.findObjectsInBackground(block: { (objects, error) in
if let object = objects?.first {
let name = object.object(forKey: "type") as? String
let caption = object.object(forKey: "caption") as? String
completionHandler(name, caption)
}else{
print(error?.localizedDescription ?? String())
completionHandler(nil, nil)
}
}
//now we can detect what object we have and show correct cell depending on object's type
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let object = tableObjects[indexPath.row]
//making downcast or if you won't use subclasses, then check type variable using switch case as I made in loadNews()
if let animal = object as? Animal,
let cell = tableView.dequeueReusableCell(withIdentifier: "AnimalCell") as? AnimalCell {
cell.captionLabel.text = animal.caption
//do additional stuff for the animal cell
//return cell
return cell
}
if let human = object as? Human,
let cell = tableView.dequeueReusableCell(withIdentifier: "HumanCell") as? HumanCell {
cell.captionLabel.text = human.caption
//do stuff with HumanCell
//return cell
return cell
}
if let element = object as? Element,
let cell = tableView.dequeueReusableCell(withIdentifier: "ElementCell") as? ElementCell {
cell.captionLabel.text = element.caption
//do stuff with ElementCell
//return cell
return cell
}
return UITableViewCell()
}
}
one simple solution, assign the data source of table view when your news loaded.. you can do that at the end of loadNews method
tableView.dataSource = self
Make sure the datasource was not assigned somewhere else like storyboard

Resources