UITableView resetting UITableViewCell after reloading - ios

For start how I've created expandable cell with UIPicker. Just in case that would be relevant to issue.
It's created in UITableView by this code
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var identifyer = ""
switch(indexPath.row){
//other cells
case 1:
//Kategoria
identifyer = "category"
categoryCell = tableView.dequeueReusableCellWithIdentifier(identifyer) as? CategoryTableViewCell
categoryCell.pickerView.delegate = self
categoryCell.pickerView.dataSource = self
return categoryCell
//other cells
}
Then I have to recognize if it's touched
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if (indexPath.row == 1){
isCategoryCellSelected = true
tableView.reloadRowsAtIndexPaths([categoryIndexPath()], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
That's how I replace text in UILabel
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
categoryCell.categoryNameLabel.text = categories[row]
}
And finaly when tableView refresh cell
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch(indexPath.row){
case 1:
if (isCategoryCellSelected){
return CategoryTableViewCell.expandedHeight
} else {
return CategoryTableViewCell.defaultHeight
}
//other heights
}
}
Defalut cell looks
Expanded cell looks
ISSUE
So when I choose item in picker then label above should have change it text and that is happening. However, when this cell shrink to default height then replace effect is gone. I have to scroll down and up to see that only then I can see changed text in label.
I assume that UITableView cache this cell and when cell is reloading then it takes this cached version. I'm only guessing. Is it how I think? How I can change this unwanted action?
SOLUTION
As #the_critic pointout my approche to save cells in vars was completly wrong. In same time recreating categoryCell every time I pick row wasn't the correct way to do it. I end up with his way of creating cell but with mine way of setting value to cell.
So it looks like this what's is chagned:
creating cell
categoryIdentifier is private let String
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell: UITableViewCell
switch(indexPath.row){
// other cells
case 1:
//Kategoria
print("recreate, selected category \(selectedCategory)")
let categoryCell = tableView.dequeueReusableCellWithIdentifier(categoryIdentifier) as! CategoryTableViewCell
categoryCell.pickerView.delegate = self
categoryCell.pickerView.dataSource = self
updateSelectedCategoryIfNeeded(categoryCell)
cell = categoryCell
// other cells
return cell;
}
private func updateSelectedCategoryIfNeeded(cell:CategoryTableViewCell) {
if let selectedCategory = self.selectedCategory{
// A category has been selected!
cell.categoryNameLabel.text = selectedCategory
updatePicekerRowPosition(cell)
}else{
// no category selected!
cell.categoryNameLabel.text = "Wybierz kategorie..."
}
}
private func updatePicekerRowPosition(cell:CategoryTableViewCell) {
if let index = categories.indexOf(selectedCategory!){
cell.pickerView.selectRow(Int(index.value), inComponent: 0, animated: false)
}
}
recognizing row selecting
categoryIndexPath is private let NSIndexPath
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedCategory = categories[row]
if let categoryCell = tableView.cellForRowAtIndexPath(categoryIndexPath) as? CategoryTableViewCell {
categoryCell.categoryNameLabel.text = selectedCategory
}
}

From what I can see in your code, there are quite a few things you misunderstand about table views.
I would try to say that more politely, but I can't. Your way of referencing a categoryCell in your code is completely wrong! UITableviewCells are not static references if you dequeue them!
FIRST STEP: Remove the categoryCell variable!!!
The way the table view works is the following:
The cellForRowAtIndexPath method takes a cell from the storyboard or your nib and reuses that cell over and over again! So, in the beginning, you may get away with doing it your way (creating a reference to categoryCell), but the situation changes as soon as you have more cells than fit on the screen, because the variable will reference a different cell!
Reading recommendation: Creating and Configuring a Table View
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : UITableViewCell?
switch(indexPath.row){
//other cells
case 1:
//Kategoria
let identifier = "category"
// tableview checks if there is a cached cell
// for the identifier (reuse),
// if there is, it will take that one!
cell = tableView.dequeueReusableCellWithIdentifier(identifier) as! CategoryTableViewCell
cell.pickerView.delegate = self
cell.pickerView.dataSource = self
if let selectedCategory = self.selectedCategory{
// A category has been selected!
cell.categoryNameLabel.text = selectedCategory
}else{
// no category selected!
cell.categoryNameLabel.text = "Wybierz kategorie..."
}
return cell
//other cells
}
As you can see above, I introduced a new variable called selectedCategory, which will reflect which category is currently selected...
Set it up like this in your controller:
var selectedCategory : String?
What happens when you reload a section or row or the whole table, is that all the UITableViewDataSource methods for the given rows are called again! In a way the table view always tries to reflect some state of your model.
You should always reflect changes in your model by reloading the row/section or the whole table (depending on what changed).
So when you pick your category, you change the model and reload your row!
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
// update the model!
selectedCategory = categories[row]
// the table view now needs to know that the model changed!
// this will trigger the dataSource method cellForRowAtIndexPath
// and because it selectedCategory is now set, it will update
// your string accordingly!
tableView.reloadRowsAtIndexPaths([/*<IndexPath of your categoryLabelCell>*/], withRowAnimation: UITableViewRowAnimation.Automatic)
}
Phew! I hope that helps...

Related

UITableViewCell dequeuereusablecellwithidentifier returns the same cell

I am creating a UITableView that enables the user to add a variable amount of data. Table looks like this initially:
When the user clicks on the "+" button, i would like to add a new cell with a UITextField for entering data. This new cell is a Custom UITableViewCell called "RecordValueCell". Here's what is looks like:
//Custom UITableViewCell
class RecordValueCell : UITableViewCell {
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var deleteButton: UIButton!
var onButtonTapped : ((_ sender : UIButton)->Void)?
#IBAction func deleteButtonTouched(_ sender: Any) {
guard let senderButton = sender as? UIButton else {
return
}
onButtonTapped?(senderButton)
}
}
However when i try to add another cell, using the tableView.dequeueReusableCell(withIdentifier: ) function, it seems to return the same cell. And here is what my UI looks like:
Empty space at the top of the section where my new cell should be. Here is the code to add the cell:
func addNewValueCell() {
guard let reusableValueCell = self.tableView.dequeueReusableCell(withIdentifier: "valueCell") as? RecordValueCell else {
fatalError("failed to get reusable cell valueCell")
}
var cell = Cell() //some custom cell Object
//add the gray horizontal line you see in the pictures
reusableValueCell.textField.addBorder(toSide: .Bottom, withColor: UIColor.gray.cgColor, andThickness: 0.5)
reusableValueCell.onButtonTapped = { (sender) in
self.removeValue(sender: sender)
}
cell.cell = reusableValueCell
self.sections[self.sections.count - 1].cells.insert(cell, at: 0)
//When i put a break point at this spot, i find that reusableValueCell is the same object as the cell that is already being used.
tableView.reloadData()
reusableValueCell.prepareForReuse()
}
When i debug it, i find that dequeueReusableCell(withIdentifier: ) returns the exact same RecordValueCell multiple times.
Here is my cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = self.sections[indexPath.section].cells[indexPath.row].cell else {
fatalError("error getting cell")
}
return cell
}
numberOfRowsInSection
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sections[section].cells.count
}
First of all, you will need to set the View Controller Class that this table is contained in as the table's UITableViewDataSource
tableView.dataSource = self // view controller that contains the tableView
Create an array of strings as member of your View Controller class which contains the data for each cell:
var strings = [String]()
Then you will need to implement the following method for the UITableViewDataSource protocol:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return strings.count
}
You should also be dequeueing the cells in your cellForRowAt method like so:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: yourIdentifier) as! YourCellClass
cell.textLabel = strings[indexPath.row]
return cell
}
Then whenever the user enters into the textField, their input will be appended to this array:
let input = textField.text
strings.append(input)
tableView.reloadData()
Once the data is reloaded, the cell will be added to the table automatically since the number of rows are defined by the String array's length and the label is set in the cellForRowAt method.
This feature is very easy to implement if you will do in a good way.
First, you have to create two TableCell. First to give the option to add a record with plus button and second for entering a value with textfield. Now always return first cell (AddRecordTableCell) in the last row in tableView, and return the number of rows according to entered values like
return totalValues.count + 1

Swift: How can I post data from three separate text fields onto labels in a custom cell, as the result of pressing a button?

I am new to swift, and I am trying to take text from one textfield and values from two other text fields (one value from each text field) on a ViewController, and then when a button is pressed, have all three pieces of information displayed horizontally in a cell in a tableview.
This is an example of what I am trying am trying to do:
I would like the data from text fields one, two and three to be displayed in labels one, two and three, once the button is pressed.
#IBAction func btn_reload(sender: AnyObject)
{
/*lbl1.text = txtfield1.text!
lbl2.text = txtfield2.text!
lbl3.text = txtfield3.text!*/
var arrmute: [AnyObject] = [AnyObject]()
var arrmute2: [AnyObject] = [AnyObject]()
var arrmute3: [AnyObject] = [AnyObject]()
arrmute.append(txtfield1.text!)
arrmute2.append(txtfield2.text!)
arrmute3.append(txtfield3.text!)
self.tbl_out.reloadData()
}
First setup set delegate and datasource of UITabelView in your storyboard.
Give reuseIdentifier of row as given in code: cell
Make outlet of your UITableView.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return arrmute.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell: UITableViewCell
if cell == nil
{
cell = UITableViewCell(style: .Default, reuseIdentifier: "cell")
}
cell.lbl1.text = arrmute[indexPath.row]
cell.lbl2.text = arrmute2[indexPath.row]
cell.lbl3.text = arrmute3[indexPath.row]
return cell
}

Change properties of previous cells in tableView

I have a tableView, designed in storyboard, that mimics a chat UI. A cell consists of:
A TextView for the message text
A Profile Image of the sender
Right now, the profile image is displayed in every cell, next to the text bubble. This is fine, but if the same users send two or more messages directly after the other, the profile image should only appear on the last bubble and not on the previous one.
I tried calling cellForRowAtIndexPath to get the previous cell's properties and change the hidden property of the profile image, but this gave me two problems:
I'm calling cellForRowAtIndexPath inside cellForRowAtIndexPath, because that's where I make the cell UI and decide wether the profile image has to be hidden or not. I don't think it's a good idea to call this Method inside itself.
Sometimes (when scrolling up and down very fast) this does not work properly.
I also tried to store all the cells in an dictionary (indexPath.row: Cell), so I can access it faster later, but this gave me the same problem namely that it does not work when scrolling up and down really fast.
This is an illustration of how it should be: http://tinypic.com/view.php?pic=2qavj9w&s=8#.Vfcpi7yJfzI
You need to both look ahead inside of your cellForRowAtIndexPath method and, as Paulw11 recommended, call reloadRowsAtIndexPaths after inserting the cell:
import UIKit
struct MyMessage {
let sender: String
let text: String
}
class MyTableViewCell: UITableViewCell {
var message: MyMessage?
var showProfileImage: Bool = false
}
class MyTableViewController: UITableViewController {
private var _messages: [MyMessage] = []
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self._messages.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let message = self._messages[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! MyTableViewCell
cell.message = message
if self._messages.count > indexPath.row + 1 {
let nextMessage = self._messages[indexPath.row + 1]
cell.showProfileImage = message.sender != nextMessage.sender
} else {
cell.showProfileImage = true
}
return cell
}
func addMessage(message: MyMessage) {
let lastIndexPath = NSIndexPath(forRow: self._messages.count - 1, inSection: 0)
let indexPath = NSIndexPath(forRow: self._messages.count, inSection: 0)
self._messages.append(message)
self.tableView.beginUpdates()
self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Bottom)
self.tableView.reloadRowsAtIndexPaths([lastIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.endUpdates()
}
}

Inserting rows in UITableView upon click

I'm having trouble adding rows to the UITableView upon UIButton click.
I have two custom-cell xibs - one that contains an UILabel, another one that contains an UIButton.
Data for the table cell is loaded from two dictionaries (answersmain and linesmain).
Here is the code for the UITableView main functions:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.linesmain["Audi"]!.count + 1
}
// 3
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.row < 3){
var cell:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
cell.lblCarName.text = linesmain["Audi"]![indexPath.row]
return cell
} else {
var celle:vwAnswers = self.tableView.dequeueReusableCellWithIdentifier("cell2") as! vwAnswers
celle.Answer.setTitle(answersmain["Good car"]![0], forState:UIControlState.Normal)
return celle
}}
What do I put here?
#IBAction func option1(sender: UIButton) {
// I need to add rows to the uitableview from two dictionaries into two different xibs
}
You can do the next:
var showingAll = false
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return showingAll ? self.linesmain["Audi"]!.count + 1 : 0
}
#IBAction func option1(sender: UIButton) {
showingAll = true
tableView.beginUpdates()
let insertedIndexPathRange = 0..<self.linesmain["Audi"]!.count + 1
var insertedIndexPaths = insertedIndexPathRange.map { NSIndexPath(forRow: $0, inSection: 0) }
tableView.insertRowsAtIndexPaths(insertedIndexPaths, withRowAnimation: .Fade)
tableView.endUpdates()
}
You should take a look over the documentation here
There is this UITableView method called insertRowsAtIndexPaths:withRowAnimation: that inserts row at a specified indexPath.
You need to modify linesmain and answersmain by adding data to these and then call [self.tableView reloadData].
It would be better if you extract linesmain["Audi"] and answersmain["Good car"] and save them into different mutable arrays and modify those.
You need to do this in the func option1.

Use selected UIPicker row as text for custom UITableViewCell UILabel SWIFT

I have been banging against this for days now, trying a number of different approaches. I have a UITableView with custom cells containing two UILabels. On selecting the cell, a UIPicker is revealed, and a variable is set referencing which cell was tapped.
Once a selection is made in the UIPicker, I need to take that value and use it to replace the existing .text of one of the cell labels. I have tried using .viewWithTag a few different ways (first tagging the label itself, then the entire cell) and nothing works.
My didSelect for the picker is:
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
println("pickerView did select row \(row)")
if(self.getStatus() == "newFirst"){
switch (editingField){
case 0:
var l:NewOrderCell! = tableView.viewWithTag(editingField) as? NewOrderCell
//the label in the custom cell is called 'Text'
l.Text.text = pickerData[row]
println("should be changing \(l.Text.text) into \(pickerData[row])")
default:
var l:NewOrderCell! = tableView.viewWithTag(editingField) as? NewOrderCell
l.Text.text = pickerData[row]
}
}
picker.hidden=true
}
and my cellForRowAtIndexPath for the table is:
var cell:NewOrderCell = tableView.dequeueReusableCellWithIdentifier("NewOrderCell") as NewOrderCell
//label
cell.Label.text = self.newOrder[indexPath.row]
//text
cell.Text.text = "Please Select"
cell.tag = indexPath.row
return cell
I know my goal, but can't get a handle on the proper way to reference a cell within a table from another method. Thanks for your help
I have come up with a solution that works, but I feel is inelegant and hard to maintain: when the picker row is selected, that text is written to the array that was used to originally populate the table (overwriting the original item), and then I reload the table, so that the new value appears. So, it's something like this:
var truth: [String:String] = ["Key1":"","Key2":"","Key3":""]
var keys:[String] = ["Key1", "Key2","Key3"]
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:NewOrderCell = tableView.dequeueReusableCellWithIdentifier("NewOrderCell") as NewOrderCell
//label
cell.Label.text = keys[indexPath.row]
//textfield
if(truth[keys[indexPath.row]] == ""){
cell.Text.text = "Please Select"
}
else{
cell.Text.text = truth[keys[indexPath.row]]
}
return cell
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
truth[keys[editingField]] = pickerData[row]
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
Happy to hear better solutions

Resources