Rick S
Rick S

Reputation: 21

Getting duplicate cells with UITableViewController cellForRowAtIndexPath

I am trying to create a Table View using a UITableViewController.

The 'cellForRowAtIndexPath' doesn't seem to be working right-- I have pre-loaded photos and comments from Parse and put them in an array, and want pictures that fill up the screen width, so I am doing some manual calculations on them to resize.

I am doing some unusual things to get the Table View to look right, but now the data is duplicated.

I know there is something here that I'm not getting, but I'm not really sure what it is... maybe I need to do something else to bind the data, or do something with the "prepareForReuse" method?

I have included code from the UITableViewController subclass and the UITableCell subclass I wrote.

import UIKit

class YouTableViewCell: UITableViewCell {

var recipeId:String = ""
var recipeName:String = ""
var imageHeight:CGFloat = 0
var labelHeight:CGFloat = 0

// DO I NEED THIS?
override func prepareForReuse() {
    self.textLabel!.text = recipeId //Put your label name
    self.imageView!.image = nil

}

override func setSelected(selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)

    // Configure the view for the selected state
 }

 }

 class YouTableViewController: BaseTableViewController { // UITableViewController {

var labelCreated = [Bool]()
var imageCreated = [Bool]()

var cellArray = [Int:YouTableViewCell]()


override func viewDidLoad() {
    super.viewDidLoad()

    self.tableView.delegate = self
    self.tableView.dataSource = self

    self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())

    if !isYouDataLoaded {
        _ = DataController.getYouPageData()
    }


    // initialize height arrays
    for _ in 0..<youFeed.count {
        labelCreated.append(false)
        imageCreated.append(false)
    }


    // Uncomment the following line to preserve selection between presentations
    // self.clearsSelectionOnViewWillAppear = false

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem()
    self.navigationController?.setToolbarHidden(false, animated: false)
}

 // ... skipping down


override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // return feedArray.count
    return youFeed.count
}



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

    print("cellForRowAtIndexPath : \(indexPath.row)")

    // Get reference to cell
    cellArray[indexPath.row] = (tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! YouTableViewCell)
    let thisCell:YouTableViewCell = cellArray[indexPath.row]!


    // TEMP -- Border
    thisCell.layer.borderWidth = 2.0
    thisCell.layer.borderColor = UIColor.blackColor().CGColor

    let meal = youFeed[indexPath.row].recipe

    // If the image exists
    if let foodImage:UIImage = meal.imageView.image! as UIImage {
        var nameLabel:UILabel = UILabel()
        var fitImageView:UIImageView?
        if imageCreated[indexPath.row] == false {
            print("For IMAGE #\(indexPath.row)")
            fitImageView = LayoutHelper.fitImageInCell(foodImage, cell: thisCell, name: meal.recipeName)
            thisCell.contentView.addSubview(fitImageView!)

            thisCell.imageHeight = fitImageView!.frame.height
            fitImageView!.frame.origin.y = 0
            print("img height : \(fitImageView!.image!.size.height)")
            imageCreated[indexPath.row] = true
        }
        if labelCreated[indexPath.row] == false {
            // Create Label
            var captionString = ""
            if let mealNameText:String = meal.recipeName as String {
                captionString += mealNameText
            }

            if let postText:String = meal.desc as String {
                captionString += "\n" + postText
            } else {
                print("Meal Desc is null")
            }
            if let numForks:Int = meal.forks as Int {
                captionString += "\n" + String(numForks) + " Forks"
            }
            if (fitImageView != nil) {
                nameLabel = LayoutHelper.fitLabelBelowImageView(captionString, imageView: fitImageView!, cell: thisCell, yOffset: 0)
                thisCell.addSubview(nameLabel)
                thisCell.labelHeight = nameLabel.frame.height
            }
        }
    } 
    return thisCell
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    let thisCell = cellArray[indexPath.row]

    var thisHt:CGFloat = 0
    if thisCell != nil {
        thisHt = thisCell!.labelHeight + thisCell!.imageHeight
    }
    return thisHt
}
}

Upvotes: 0

Views: 1688

Answers (1)

markwatsonatx
markwatsonatx

Reputation: 3501

When you re-use cells (which is a good thing to do!) you must re-initialize the entire cell every time cellForRowAtIndexPath is called. This is because it is possible that when you make this call:

tableView.dequeueReusableCellWithIdentifier

it could be returning a cell that you have already initialized. For example, let's say I have 4 rows visible on the screen:

Row 1 - Cell A [Image 1]
Row 2 - Cell B [Image 2]
Row 3 - Cell C [Image 3]
Row 4 - Cell D [Image 4]

Now, let's say I scroll down and Row 1 is no longer on the screen, but Row 5 has now appeared. It is possible that the cell you receive for Row 5 is the same exact cell that was at Row 1. So, immediately after the call to tableView.dequeueReusableCellWithIdentifier Row 5 looks like this:

Row 2 - Cell B [Image 2]
Row 3 - Cell C [Image 3]
Row 4 - Cell D [Image 4]
Row 5 - Cell A [Image 1] <--

You need to update Cell A to point to Image 5. That may work fine in your code and you get this:

Row 2 - Cell B [Image 2]
Row 3 - Cell C [Image 3]
Row 4 - Cell D [Image 4]
Row 5 - Cell A [Image 5] <--

Now, let's say you scroll back up and Row 1 is assigned Cell A again:

Row 1 - Cell A [Image 5] <--
Row 2 - Cell B [Image 2]
Row 3 - Cell C [Image 3]
Row 4 - Cell D [Image 4]

Since you are storing arrays that keep track of which rows have been initialized (imageCreated, labelCreated) the cell won't get updated to point to Image 1:

// THIS WILL BE TRUE, SO IT WILL NOT EXECUTE
if imageCreated[indexPath.row] == false

This means get rid of your imageCreated and labelCreated arrays and perform the code to initialize your cells every time cellForRowAtIndexPath is called.

Also, I would recommend getting rid of the cellArray and not storing references to cells (for the same reasons I mentioned above). You can cache cell heights and that should be fine because those are not tied to re-usable cells. You could do something like this:

var cellHeightArray = [Int:CGFloat]()

Since you are doing calculations in cellForRowAtIndexPath set the height at the bottom:

cellHeightArray[indexPath.row] = thisCell!.labelHeight + thisCell!.imageHeight;

Then set it from the array in heightForRowAtIndexPath:

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    let thisCellHeight = cellHeightArray[indexPath.row]

    var thisHt:CGFloat = 0
    if thisCellHeight != nil {
        thisHt = thisCellHeight
    }
    return thisHt
}

By default, however I believe heightForRowAtIndexPath is called before cellForRowAtIndexPath. In that case some of your cells will have a height of 0 until you reload the tableview. Look at this post for more information:

Why does heightForRowAtIndexPath: come before cellForRowAtIndexPath:?

Upvotes: 2

Related Questions