iosswiftuitableviewpfquery

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

Solution

  • 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:

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