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