nontomatic
nontomatic

Reputation: 2043

Image added to UITableViewCell appears in wrong rows when scrolling

I'm adding an image to a table view row (actually, I seem to be adding it to the row's cell) when selecting it (and removing when selecting it again). The table view consists of prototype cells.

This works but when I scroll around and get back to the row I had previously selected, the image would be in another row. Also, the image appears in other rows as well.

My guess is this happens because the cells are re-used when scrolling. Here's the code of a little sample project:

import UIKit

class MyTableViewController: UITableViewController {

  // Using integers for simplicity, should work with strings, too.
  var numbers = [Int]()

  override func viewDidLoad() {
    super.viewDidLoad()

    for i in 0..<50 {
      numbers.append(i)
    }
  }

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("TestCell", forIndexPath: indexPath)
    cell.textLabel?.text = "\(numbers[indexPath.row] + 1)"

    return cell
  }

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

  override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let cell = tableView.cellForRowAtIndexPath(indexPath)!

    if let myImage = curCell.viewWithTag(10) as? MyImage {
      myImage.removeFromSuperview()
    } else {
      let myImage = myImage()
      myImage.tag = 10
      cell.addSubview(myImage)
    }

  }

I need to have the image stay in the correct row, also when coming back to this view controller. What's the correct way to tackle this? Any advice much appreciated!

EDIT: I've tried to implement matt's answer but I seem to be missing something, as the problem is still the same.

EDIT 2: Updated, working as intended now.

import UIKit

class ListItem {

  var name: String!
  var showsImage = false

  init(name: String) {
    self.name = name
  }

}


class MyTableViewController: UITableViewController {

  var listItems = [ListItem]()

  override func viewDidLoad() {
    super.viewDidLoad()

    for i in 0..<50 {
      let listItem = ListItem(name: "row \(i)")
      listItems.append(listItem)
    }
  }


  // MARK: - Table view data source

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("TestCell", forIndexPath: indexPath)

    cell.textLabel?.text = "\(listItems[indexPath.row].name)"

    if listItems[indexPath.row].showsImage {
      let myImage = myImage
      myImage.tag = 10
      cell.addSubview(myImage)
    } else {
      if let myImage = cell.viewWithTag(10) as? myImage {
      myImage.removeFromSuperview()
      listItems[indexPath.row].showsImage = false
    }

    return cell
  }

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

  override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let cell = tableView.cellForRowAtIndexPath(indexPath)!

    if let myImage = cell.viewWithTag(10) as? myImage {
      myImage.removeFromSuperview()
      listItems[indexPath.row].showsImage = false
    } else {
      listItems[indexPath.row].showsImage = true
      tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
    }
  }

}

EDIT 3: As matt suggested, here's an alternative solution to the code above which subclasses UITableViewCell instead of using a tag for the image.

import UIKit

class MyTableViewCell: UITableViewCell {

  var myImage = MyImage()

  override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    myImage.hidden = true
    addSubview(myImage)
  }

  required init?(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
  }

}


class ListItem {

  var name: String!
  var showsImage = false

  init(name: String) {
    self.name = name
  }

}

class MyTableViewController: UITableViewController {

  var listItems = [ListItem]()

  override func viewDidLoad() {
    super.viewDidLoad()

    for i in 0..<50 {
      let listItem = ListItem(name: "row \(i)")
      listItems.append(listItem)
    }
  }


  // MARK: - Table view data source

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

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = MyTableViewCell(style: .Default, reuseIdentifier: "TestCell")

    cell.textLabel?.text = "\(listItems[indexPath.row].name)"
    cell.myImage.hidden = !(listItems[indexPath.row].showsImage)

    return cell
  }

  override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let cell = tableView.cellForRowAtIndexPath(indexPath) as! MyTableViewCell

    listItems[indexPath.row].showsImage = cell.myImage.hidden
    cell.myImage.hidden = !cell.myImage.hidden
  }

}

Upvotes: 2

Views: 1144

Answers (1)

matt
matt

Reputation: 535138

The problem is that cells are reused in other rows. When they are, cellForRowAtIndexPath is called again. But when it is, you are supplying no information about the image for that row.

The solution: fix your model (i.e. the info you consult in cellForRowAtIndexPath) so that it knows about the image. In didSelect, do not modify the cell directly. Instead, modify the model and reload the cell.

Upvotes: 5

Related Questions