GuiSoySauce
GuiSoySauce

Reputation: 1783

UITableView: Defer reloading until all data has downloaded in the background

I've been struggling to get my tableview to load the .count correctly. I got to find a way to tell the tableview only to only load after my image and post arrays are fully populated.

Otherwise I will keep getting a

fatal error: Array index out of range

at the

cell.cellImage?.image = imagesArray[indexPath.row]

inside

cellForRowAtIndexPath

Output:

    NUMBER OF POSTS->0
    NUMBER OF IMAGES->0
    NUMBER OF POSTS->0
    NUMBER OF IMAGES->0
    NUMBER OF POSTS->0
    NUMBER OF IMAGES->0
    POSTSARRAY COUNT->1
    POSTSARRAY COUNT->2
    POSTSARRAY COUNT->3
    POSTSARRAY COUNT->4

    NUMBER OF POSTS->4
    NUMBER OF IMAGES->0

    IMAGESARRAY COUNT->1
    IMAGESARRAY COUNT->2
    IMAGESARRAY COUNT->3
    IMAGESARRAY COUNT->4

    NUMBER OF POSTS->4
    NUMBER OF IMAGES->4

Code

    override func viewDidLoad() {
        super.viewDidLoad()

//        myTableView.estimatedRowHeight = 312.0
//        myTableView.rowHeight = UITableViewAutomaticDimension

        var query = PFQuery(className: "Post")
        query.whereKey("hobbieTag", equalTo:"\(selectedHobbie)")
        query.orderByAscending("description")
        query.findObjectsInBackgroundWithBlock
            {
                (objects: [AnyObject]?, error: NSError?) -> Void in

                if error == nil
                {
                    //                println("HOBBIES.COUNT->\(hobbies?.count)")
                    for post in objects!
                    {
                        //GET POST TITLE
                        self.posts.append(post["postText"] as! String)
                        println("POSTSARRAY COUNT->\(self.posts.count)")

                        //TEST IMAGE
                        //var appendImage = UIImage(named: "logoPDF")
                        //self.imagesArray.append(appendImage!)

                        //GET IMAGE FILE
                        let postImageFile = post["postImage"] as? PFFile
                        postImageFile?.getDataInBackgroundWithBlock({ (imageData: NSData?, error: NSError?) -> Void in

                            var image = UIImage(data: imageData!)
                            self.imagesArray.append(image!)
                            println("IMAGESARRAY COUNT->\(self.imagesArray.count)")

                            }, progressBlock: { (progress: Int32) -> Void in
//                                println("PROGRESS->\(progress)")
                        })
                    }
                    self.myTableView.reloadData()
                }
                else
                {
                    println(error?.localizedDescription)
                    println(error?.code)
                }
//                self.myTableView.reloadData()
        }//END query
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    override func viewDidAppear(animated: Bool) {
        myTableView.reloadData()
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        println("NUMBER OF POSTS->\(posts.count)")
        println("NUMBER OF IMAGES->\(imagesArray.count)")
        return posts.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cellIdentifier = "Cell"
        let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! HobbieFeedTableViewCell

        cell.cellTitle.text = posts[indexPath.row]
        cell.cellSubtitle.text = posts[indexPath.row]


        //        cell.cellImage.image = UIImage(named: "logoPDF")
        //cell.cellImage?.image = imagesArray[indexPath.row]

        return cell
    }

Updated code

        var query = PFQuery(className: "HobbieFeed")
        query.whereKey("hobbieTag", equalTo:"\(selectedHobbie)")
        query.orderByAscending("description")
        query.findObjectsInBackgroundWithBlock
            {
                (objects: [AnyObject]?, error: NSError?) -> Void in

                if error == nil
                {
                    let semaphore = dispatch_semaphore_create(0)

                    //                println("HOBBIES.COUNT->\(hobbies?.count)")
                    for post in objects!
                    {
                        //GET POST TITLE
                        self.posts.append(post["postText"] as! String)
                        println("POSTSARRAY COUNT->\(self.posts.count)")

                        //TEST IMAGE
                        //var appendImage = UIImage(named: "logoPDF")
                        //self.imagesArray.append(appendImage!)

                        //GET IMAGE FILE
                        let postImageFile = post["postImage"] as? PFFile
                        postImageFile?.getDataInBackgroundWithBlock({ (imageData: NSData?, error: NSError?) -> Void in

                            var image = UIImage(data: imageData!)
                            self.imagesArray.append(image!)
                            println("IMAGESARRAY COUNT->\(self.imagesArray.count)")

                            dispatch_semaphore_signal(semaphore)

                            }, progressBlock: { (progress: Int32) -> Void in
                                println("PROGRESS->\(progress)")
                        })


                    }

                    // Wait for all image loading tasks to complete
                    for post in objects! {
                        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
                        println("SHAPHORE POSTS COUNT->\(self.posts.count)")
                        println("SEMAPHORE IMAGES COUNT->\(self.imagesArray.count)")
                    }

                    self.myTableView.reloadData()
                }

        }//END query

Upvotes: 3

Views: 1889

Answers (3)

GuiSoySauce
GuiSoySauce

Reputation: 1783

Finally got around the issue. May be helpful to other users so will post the answer here.

Toke a different approach, simpler I believe.

The query happens on view did load, but there is no separate array for images or content at that stage. The cell gets the entire query object array and then retrieves what it needs for each row. Also easier to detect if there is image or not.

Global array:

var timelineData = NSMutableArray()

View did load query:

 var query = PFQuery(className: "Feed")
    query.whereKey("Tag", equalTo:"\(selected)")
    query.orderByAscending("description")
    query.findObjectsInBackgroundWithBlock
        {
            (objects: [AnyObject]?, error: NSError?) -> Void in

            if error == nil
            {
                for object in objects!
                {
                    self.timelineData.addObject(object)
                }
                self.myTableView.reloadData()
            }
    }//END query

tableview cellforrow

 func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        //Cell identifiers
        let standardCellIdentifier = "StandardCell"
        let imageCellIdentifier = "ImageCell"

        //get object, text and votes
        let object = self.timelineData.objectAtIndex(indexPath.row) as! PFObject
        var myText = object.objectForKey("postText") as? String
        var hasImage = object.objectForKey("hasImage") as? Bool
        var myVotes = object.objectForKey("votes") as! Int
        let imageFromParse = object.objectForKey("postImage") as? PFFile

        //if image
        if hasImage == true
        {
            let imageCell = tableView.dequeueReusableCellWithIdentifier(imageCellIdentifier, forIndexPath: indexPath) as! FeedImageTVCell

            //set image for cell
            imageFromParse!.getDataInBackgroundWithBlock({ (imageData:NSData?, error:NSError?) -> Void in
                if error == nil {

                    if let myImageData = imageData {
                        let image = UIImage(data:myImageData)
                        imageCell.cellImage!.image = image
                    }
                }
                }, progressBlock: { (percent: Int32) -> Void in
            })
            imageCell.cellVotes.text = "Votes - \(myVotes)"
            imageCell.cellText.text = myText
            return imageCell
        }
        //if no image
        else
        {
            let standardCell = tableView.dequeueReusableCellWithIdentifier(standardCellIdentifier, forIndexPath: indexPath) as! FeedStandardTVCell
            standardCell.cellVotes.text = "Votes - \(myVotes)"
            standardCell.cellText.text = myText
            return standardCell

        }
    }

Upvotes: 0

Lars Christoffersen
Lars Christoffersen

Reputation: 1739

Hi As far as I can see you are doing the right thing, loading the data asynchronously in the background. I think where it fails is in the asynchronous block where you reload the table. Try this code there:

dispatch_async(dispatch_get_main_queue()){self.myTableView.reloadData()}

The block is actually running in af different thread, thus it is not at all synchronised with your other code. If you are using test this is also a problem to get the asynchronous blocks tested properly. Here you can use the waitForExpectationsWithTimeout(seconds) to test you code properly

Upvotes: 1

hennes
hennes

Reputation: 9342

You're kicking of the image loading in the background in a loop and call reloadData right after the loop. The background tasks are not finished at this point, however.

for post in objects! {
    ...
    // This starts a background operation
    postImageFile?.getDataInBackgroundWithBlock(...)
    ...
}
// The background tasks are not necessarily completed at this point
self.myTableView.reloadData()

To wait until all background tasks have finished you can use semaphores. Here is a basic example.

// Create a new semaphore with a value of 0
let semaphore = dispatch_semaphore_create(0)

// Kick off a bunch of background tasks
for i in 0...10 {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
        sleep(1) // Do some work or sleep

        // Signal the semaphore when the task is done. This increments
        // the semaphore.
        dispatch_semaphore_signal(semaphore)
    }
}

// Wait until all background tasks are done
for i in 0...10 {
    // This waits until the semaphore has a positive value
    // and then decrements it
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}

// This is only executed after all background tasks are done
println("all tasks are done")

Note that this example could be simplified by using dispatch groups. This is, however, not an option in your case since your calling a function with a completion handler instead of executing a block on a queue directly.

Applying the above approach to your code would look like this.

let semaphore = dispatch_semaphore_create(0) // Create a semaphore (value: 0)

for post in objects! {
    ...
    postImageFile?.getDataInBackgroundWithBlock({ (imageData: NSData?, error: NSError?) -> Void in
        ... // Do your work

        // Increment the semaphore when the image loading is completed
        dispatch_semaphore_signal(semaphore)
    }, progressBlock: {
        ...
    })
    ...
}

// Wait for all image loading tasks to complete
for post in objects! {
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}

// This is only called after all images have loaded
self.myTableView.reloadData()

Upvotes: 4

Related Questions