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
}
}
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.
Related
my cells are not appearing.
I did:
Checked if datasource and delegate were connected
Checked if my custom cells identifier name and class were correct
Things that I didn't:
I am struggling with auto layout, so I just decided not to do it.
My app is loading with the correct amount of cells, but the cells are not registered.
My code:
import UIKit
class WelcomeViewController: UITableViewController, NetworkManagerDelegate {
private var networkManager = NetworkManager()
private var infoForCells = [Result]()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UINib(nibName: "ImageViewCell", bundle: nil), forCellReuseIdentifier: "imageCell")
networkManager.delegate = self
networkManager.fetchNews()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return infoForCells.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "imageCell", for: indexPath) as? ImageViewCell else{
return UITableViewCell(style: .default, reuseIdentifier: "cell")
}
let cellIndex = infoForCells[indexPath.row]
cell.titleForImage.text = cellIndex.alt_description
print(cell.titleForImage ?? "lol")
// if let image = cellIndex.urlToImage {
// cell.imageForArticle.load(url: image)
// }
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
func didUpdateNews(root: Root) {
infoForCells = root.results
}
}
Reload the table
func didUpdateNews(root: Root) {
infoForCells = root.results
tableView.reloadData()
}
In addition to Sh_Khan answer you can also listen to updates of infoForCells property
private var infoForCells = [Result]() {
didSet {
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
}
}
I ran into a problem recently.
I'll post some code too. Tried the methods from internet but without success. Also, I have linked the 2 tables to the View Controller.
Here is some code.
class ViewController2: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var carImageView: UIImageView!
#IBOutlet weak var pieseDorite: UITableView!
#IBOutlet weak var pieseDisponibile: UITableView!
var fullNameArr : [String] = [] // i populate the string in a action button, no problem with the arrays
var fullNameArrDorit : [String] = []
override func viewDidLoad()
{
super.viewDidLoad()
carImageView.image = UIImage(named: ("simplu"))
carImageView.contentMode = .ScaleAspectFit
pieseDisponibile.delegate = self
pieseDisponibile.dataSource = self
self.view.addSubview(pieseDisponibile)
pieseDorite.delegate = self
pieseDorite.dataSource = self
self.view.addSubview(pieseDorite)
}
func tableView(pieseDisponibile: UITableView, numberOfRowsInSection section:Int) -> Int
{
return fullNameArr.count
}
func tableView(pieseDisponibile: UITableView, pieseDorite: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell:UITableViewCell=UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell")
cell.textLabel!.text = String(fullNameArr[indexPath.row])
return cell
}
func tableView2(pieseDorite: UITableView, numberOfRowsInSection section:Int) -> Int
{
return fullNameArrDorit.count
}
func tableView2(pieseDorite: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell2:UITableViewCell=UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell2")
cell2.textLabel!.text = String(fullNameArrDorit[indexPath.row])
return cell2
}
}
Also, tried to add all other functions, the problem persisted. I think I have linked something bad, I don't know.
Replace all tableView2 with tableView please, and leave only 1 each method. You need have same delegate methods for all UITableView instances. You can separate different instances inside this methods by comparing first parameter with your UITableView property. Also add number of section method:
override func viewDidLoad()
{
super.viewDidLoad()
carImageView.image = UIImage(named: "simplu")
carImageView.contentMode = .ScaleAspectFit
pieseDisponibile.delegate = self
pieseDisponibile.dataSource = self
view.addSubview(pieseDisponibile)
pieseDorite.delegate = self
pieseDorite.dataSource = self
view.addSubview(pieseDorite)
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {return 1}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if tableView == pieseDisponibile {return fullNameArr.count}
return fullNameArrDorit.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
if tableView == pieseDisponibile {
let cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell")
cell.textLabel?.text = String(fullNameArr[indexPath.row])
return cell
}
let cell2 = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell2")
cell2.textLabel?.text = String(fullNameArrDorit[indexPath.row])
return cell2
}
I made an app with UITableView. I want to show a tick mark in the left when cell is touched, and to be hidden when again is touched. I used some code and It's not showing as I wanted. The tick is showing on the right not on the left of screen.
This is how I want to make:
This is how it is:
And here is code that I used:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
self.myTableView.allowsMultipleSelection = true
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.cellForRowAtIndexPath(indexPath)?.accessoryType = UITableViewCellAccessoryType.Checkmark
}
func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
tableView.cellForRowAtIndexPath(indexPath)?.accessoryType = UITableViewCellAccessoryType.None
}
In conclusion, first problem is that the tick is showing on the right of the cell and not on the left. And the other problem, it won't untick (hide when cell is pressed again)
Thanks.
Try this
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "DhikrTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! GoalsTableViewCell
cell.tickButton.addTarget(self, action: #selector(GoalsViewController.toggleSelcted(_:)), forControlEvents: UIControlEvents.TouchUpInside)
cell.tickButton.tag = indexPath.row
// Fetches the appropriate meal for the data source layout.
let workout = workouts[indexPath.row]
let number = numbers[indexPath.row]
if workout.isSelected {
cell.tickButton.setImage(UIImage(named: "Ticked Button"), forState: UIControlState.Normal)
} else {
cell.tickButton.setImage(UIImage(named: "Tick Button"), forState: UIControlState.Normal)
}
cell.nameLabel.text = workout.name
cell.numberLabel.text = number.number
return cell
}
func toggleSelcted(button: UIButton) {
let workout = workouts[button.tag]
workout.isSelected = !workout.isSelected
myTableView.reloadData()
}
To achieve this behavior with default UITableViewCell, you may need to set table view to edit mode.
Try the following code in your viewDidLoad.
self.tableView.allowsMultipleSelectionDuringEditing = true
self.tableView.setEditing(true, animated: false)
This will show tick mark on the left side, and also this will untick (hide) when cell is pressed again.
Edited:
If you need more customization, Create a custom table view cell and handle the selection/tick mark manually.
This will be your cell.swift.
class CustomCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var tickImageView: UIImageView!
//Handles the cell selected state
var checked: Bool! {
didSet {
if (self.checked == true) {
self.tickImageView.image = UIImage(named: "CheckBox-Selected")
}else{
self.tickImageView.image = UIImage(named: "CheckBox-Normal")
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
checked = false
self.layoutMargins = UIEdgeInsetsZero
self.separatorInset = UIEdgeInsetsZero
}
The datasource array is,
let list = ["Title 1", "Title 2", "Title 2", "Title 4"]
The view controller didLoad will be like
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
listView.registerNib(UINib(nibName: "OptionsSelectionCell", bundle: nil), forCellReuseIdentifier: "SelectionCell")
}
The view controller table delegate methods will be like,
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SelectionCell") as! OptionsSelectionCell
cell.titleLabel.text = list[indexPath.row]
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! OptionsSelectionCell
cell.checked = !cell.checked
}
}
I know this topic has been raised quite a few times but I can't seem to get through.
This is a simple UITableViewController who's cells are either normal UITableViewCells or custom ones.
This allow users to input text in a textField in a cell and add it directly to the tableView.
When the user is done entering his text he hits return and the new entry gets added to listOfItems. At this point I want my tableView to reload its data to show the last item added.
Doesn't seem to work.
class CreateListTableViewController: UITableViewController {
// MARK: - Model
var listOfItems = [String]()
{
didSet {
tableView.reloadData()
println(listOfItems)
}
}
// MARK: - View LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBarHidden = false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listOfItems.count + 1
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row + 1 == listOfItems.count{
let cell = tableView.dequeueReusableCellWithIdentifier("tableCellSearch", forIndexPath: indexPath) as! ListItemTableViewCell
cell.configure(text: "", placeholder: "Enter an item")
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath) as! UITableViewCell
println(indexPath.row)
cell.textLabel?.text = listOfItems[indexPath.row]
return cell
}
}
}
Here's the code for the custom UITableViewCell:
class ListItemTableViewCell: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var cellTextField: UITextField!
var controllerArray = CreateListTableViewController()
func configure(#text: String?, placeholder: String){
cellTextField.text = text
cellTextField.placeholder = placeholder
cellTextField.accessibilityValue = text
cellTextField.accessibilityLabel = placeholder
}
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {
textField.text = ""
return true
}
override func awakeFromNib() {
super.awakeFromNib()
cellTextField.delegate = self
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
controllerArray.listOfItems += [textField.text]
textField.text = ""
return true
}
func textFieldShouldEndEditing(textField: UITextField) -> Bool {
return true
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
Interestingly reloadData calls 'numberOfRows' but not 'cellForRow'.
Your indexes/sizes are off such that your code only works when listOfItems' size is 0. Try this:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listOfItems.count + 1
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row == listOfItems.count {
let cell = tableView.dequeueReusableCellWithIdentifier("tableCellSearch", forIndexPath: indexPath) as! ListItemTableViewCell
cell.configure(text: "", placeholder: "Enter an item")
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath) as! UITableViewCell
println(indexPath.row)
cell.textLabel?.text = listOfItems[indexPath.row]
return cell
}
}
Update:
OK, the problem is that in ListItemTableViewCell you are creating a new instance of CreateListTableViewController and assigning it to controllerArray. This means that when you call controllerArray.listOfItems += [textField.text], you are calling it on a separate, second instance of CreateListTableViewController that is not on the screen.
Instead, you should leave controllerArray uninitialized in ListItemTableViewCell and then set it from cellForRowAtIndexPath like this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if indexPath.row + 1 == listOfItems.count{
let cell = tableView.dequeueReusableCellWithIdentifier("tableCellSearch", forIndexPath: indexPath) as! ListItemTableViewCell
cell.configure(text: "", placeholder: "Enter an item")
cell.controllerArray = self
return cell
} else {
let cell = tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath) as! UITableViewCell
println(indexPath.row)
cell.textLabel?.text = listOfItems[indexPath.row]
return cell
}
}
Can you make sure that your internal list is updated correctly and the numberOfRowsInSection functions returns number other than 0 or 1?
I have a login screen - after login, I am displaying list in a table view with check boxes. After submitting button. all the selected check box labels should send to server. Login screen and displaying table working how to write action for submit import UIKit
class ViewController: UIViewController,UITableViewDataSource, UITableViewDelegate {
private let dwarves = [
"Sleepy", "Sneezy", "Bashful", "Happy",
"Doc", "Grumpy", "Dopey",
"Thorin", "Dorin", "Nori", "Ori",
"Balin", "Dwalin", "Fili", "Kili",
"Oin", "Gloin", "Bifur", "Bofur",
"Bombur"]
let simpleTableIdentifier = "SimpleTableIdentifier"
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return dwarves.count
}
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier(
simpleTableIdentifier) as? UITableViewCell
if (cell == nil) {
cell = UITableViewCell(
style: UITableViewCellStyle.Default, reuseIdentifier: simpleTableIdentifier)
}
let image = UIImage(named: "unchecked")
cell!.imageView.image = image
let highlightedImage = UIImage(named: "checked")
cell!.imageView.highlightedImage = highlightedImage
cell!.textLabel.text = dwarves[indexPath.row]
return cell!
}
/*func tableView(tableView: UITableView,
didSelectRowAtIndexPath indexPath: NSIndexPath) {
let rowValue = dwarves[indexPath.row]
for element in rowValue {
sel = [rowValue]
println(element)
}
}*/
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
let rowValue = dwarves[indexPath.row]; println("You selected cell \(indexPath.row)")
}
}
You can create an NSMutableArray instance variable to hold the selected values:
var selected = NSMutableArray()
Then, in your didSelectRowAtIndexPath method, add to the array if the object does not exist or remove the object if it does exist.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let rowValue = dwarves[indexPath.row]; println("You selected cell \(indexPath.row)")
// if the item exsits then remove it
if selected.containsObject(rowValue) {
selected.removeObject(rowValue)
} else { // else the item doesn't exist so add it
selected.addObject(rowValue)
}
println(selected)
}