Custom UITableViewCell changes when scrolled - ios

I have a bar button item which inserts new rows with an incrementing integer variable:
class TableViewController: UITableViewController {
var personNo = 0
var data = [String]()
#IBAction func addPerson(_ sender: UIBarButtonItem) {
personNo += 1
tableView.beginUpdates()
data.append("Person \(personNo)")
tableView.insertRows(at: [IndexPath(row: data.count - 1, section: 0)], with: .automatic)
tableView.endUpdates()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "newPerson") as! CustomCell
cell.lblPerson?.text = "Person \(personNo): "
// Configure the cell...
return cell
}
}
Adding the rows work, but the cell's value changes when the table view is scrolled:
Why is this happening and how can I save the state of each cell?

You only have a single personNo variable, so when cells are generated for scrolling, the current value of personNo is used.
You can use the indexPath.row value:
cell.lblPerson?.text = "Person \(indexPath.row+1): "

You need to get the data from the data source array (data)
Replace
cell.lblPerson?.text = "Person \(personNo): "
with
cell.lblPerson?.text = data[indexPath.row]
Side-note: for your purpose I recommend to use a custom model for example:
struct Person {
var name : String
var amount : Double
}

Related

UITableView reloadData() causing reload to UISearchBar inside every sections

I have a tableview which has 2 sections. Both of the sections have UISearchBar in the indexPath.row 0 and the rest of the rows in each section populate the list of array.
Whenever I type some text in search bar every time the searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) delegate method gets called and inside the delegate method I call tableView.reloadData() to reload the search results in tableview.
Now the problem is each time the tableView reloads the UISearchBar reloads too (as UISearchbar is in row number 1) and every time the SearchBar keypad Resigns.
Instead of doing tableView.reloadData() I even tried to reload every row except the first one using bellow code
let allButFirst = (self.tableView.indexPathsForVisibleRows ?? []).filter { $0.section != selectedSection || $0.row != 0 }
self.tableView.reloadRows(at: allButFirst, with: .automatic)
But no luck. App gets crashed saying
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert row 2 into section 0, but there are only 2 rows in section 0 after the update'
You are probably changing the data source and then you are reloading rows at index paths what doesn't exist yet.
It is not so easy, but let's have an example: Before you start typing, the search result will contain something like this:
["aa", "ab", "ba", "bb"]
Then you will type "a" to the search bar and data source changes into:
["aa", "ab"]
tableView.deleteRows(at: [IndexPath(row:3, section: 0), IndexPath(row:4, section: 0)], with: .automatic)
then you delete everything in this searchbar and your data source will change to the default: ["aa", "ab", "ba", "bb"]
so in this case you need to call:
tableView.insertRows(at: [IndexPath(row:3, section: 0), IndexPath(row:4, section: 0)], with: .automatic)
I created some working example - without storyboard source, I believe it is pretty simple to recreated it according this class.
class SearchCell: UITableViewCell {
#IBOutlet weak var textField:UITextField?
}
class TextCell: UITableViewCell {
#IBOutlet weak var label:UILabel?
}
class ViewController: UIViewController, UITableViewDataSource, UITextFieldDelegate {
#IBOutlet weak var tableView: UITableView?
weak var firstSectionTextField: UITextField?
var originalDataSource:[[String]] = [["aa","ab","ba","bb"], ["aa","ab","ba","bb"]]
var dataSource:[[String]] = []
let skipRowWithSearchInput = 1
override func viewDidLoad() {
super.viewDidLoad()
dataSource = originalDataSource
tableView?.tableFooterView = UIView()
tableView?.tableHeaderView = UIView()
}
func numberOfSections(in tableView: UITableView) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource[section].count + skipRowWithSearchInput
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0, let cell = tableView.dequeueReusableCell(withIdentifier: "search", for: indexPath) as? SearchCell {
cell.textField?.removeTarget(self, action: #selector(textFieldDidChangeText(sender:)), for: .editingChanged)
cell.textField?.addTarget(self, action: #selector(textFieldDidChangeText(sender:)), for: .editingChanged)
if indexPath.section == 0 {
firstSectionTextField = cell.textField
}
return cell
} else if let cell = tableView.dequeueReusableCell(withIdentifier: "text", for: indexPath) as? TextCell {
cell.label?.text = dataSource[indexPath.section][indexPath.row - skipRowWithSearchInput]
return cell
} else {
return UITableViewCell()
}
}
#objc func textFieldDidChangeText(sender: UITextField) {
let section = sender == firstSectionTextField ? 0 : 1
let text = sender.text ?? ""
let oldDataSource:[String] = dataSource[section]
//if the search bar is empty then use the original data source to display all results, or initial one
let newDataSource:[String] = text.count == 0 ? originalDataSource[section] : originalDataSource[section].filter({$0.contains(text)})
var insertedRows:[IndexPath] = []
var deletedRows:[IndexPath] = []
var movedRows:[(from:IndexPath,to:IndexPath)] = []
//resolve inserted rows
newDataSource.enumerated().forEach { (tuple) in let (toIndex, element) = tuple
if oldDataSource.contains(element) == false {
insertedRows.append(IndexPath(row: toIndex + skipRowWithSearchInput, section: section))
}
}
//resolve deleted rows
oldDataSource.enumerated().forEach { (tuple) in let (fromIndex, element) = tuple
if newDataSource.contains(element) == false {
deletedRows.append(IndexPath(row: fromIndex + skipRowWithSearchInput, section: section))
}
}
//resolve moved rows
oldDataSource.enumerated().forEach { (tuple) in let (index, element) = tuple
if newDataSource.count > index, let offset = newDataSource.firstIndex(where: {element == $0}), index != offset {
movedRows.append((from: IndexPath(row: index + skipRowWithSearchInput, section: section), to: IndexPath(row: offset + skipRowWithSearchInput, section: section)))
}
}
//now set dataSource for uitableview, right before you are doing the changes
dataSource[section] = newDataSource
tableView?.beginUpdates()
if insertedRows.count > 0 {
tableView?.insertRows(at: insertedRows, with: .automatic)
}
if deletedRows.count > 0 {
tableView?.deleteRows(at: deletedRows, with: .automatic)
}
movedRows.forEach({
tableView?.moveRow(at: $0.from, to: $0.to)
})
tableView?.endUpdates()
}
}
the result:
If do you need to clarify something, feel free to ask in comment.
Try this-
tableView.beginUpdates()
//Do the update thing
tableView.endUpdates()
It worked.
I took two sections one for search field and another for reloading data (rows populating data).
I took separate custom cell for search and took outlet in that class itself.
and in viewForHeaderInSection I used tableView.dequeueReusableCell(withIdentifier:) and returned customCell.contentView
Then I called tableview.ReloadData() in searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
It worked without problem.

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

Some table view cells become invisible after I added more cells. Why?

I am using a variation of the technique mentioned in this post to add and remove table view cells dynamically.
Initially, the table view cells looks like this:
Then, I add a new cell to section 1. Section 1 is the section above the "RESULTS" section. So I expect the new cell to appear below the cell with the name "h". But no! It turns into this!
The new cell is added in section 2 (The "RESULTS" section) and is added below the cell with the name "b". What's even more surprising is that the second cell in section 2 has disappeared!
Here is how I add the cell:
I have an array of cells here:
var cells: [[UITableViewCell]] = [[], [], []]
each subarray in the array represents a section. In viewDidLoad, I added some cells to sections 0 to 2 by calling:
addCellToSection(1, cell: someCell)
addCellToSection is defined as
func addCellToSection(section: Int, cell: UITableViewCell) {
cells[section].append(cell)
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: cells[section].endIndex - 1, inSection: section)], withRowAnimation: .Top)
}
And the table view data source methods are defined in the same way as the aforementioned post.
I tried to print the number of cells in each section when I add the cell:
print("no. of rows in section 1: \(self.tableView(tableView, numberOfRowsInSection: 1))")
print("no. of rows in section 2: \(self.tableView(tableView, numberOfRowsInSection: 2))")
And the printed values are consistent i.e. when I add a new cell, the no. of rows increase by 1. But the weird thing is that it keeps placing rows in the wrong position.
Extra info: how I create the cell:
I first dequeue the cells from the prototype cells. I then call viewWithTag to get the text fields that are in the cell and add them to a [(UITextField, UITextField)]. Don't know whether this matters.
Okay so first of all, you should never store UITableView cells in some custom collection. This is and should be done by iOS, not you.
The data you are using to populate the cells are stored in some model I presume?
Your tableView should register cells using either:
func registerClass(cellClass: AnyClass?, forCellReuseIdentifier identifier: String)
or
func registerNib(nib: UINib?, forCellReuseIdentifier identifier: String)
or using Prototype cells in the Xib/Storyboard.
I recommend this setup, or similar:
class MyModel {
/* holds data displayed in cell */
var name: String?
var formula: String?
init(name: String, formula: String) {
self.name = name
self.formula = formula
}
}
class MyCustomCell: UITableViewCell, UITextFieldDelegate {
static var nibName = "MyCustomCell"
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var formulaTextField: UITextField!
weak var model: MyModel?
override func awakeFromNib() {
super.awakeFromNib()
nameTextField.delegate = self
formulaTextField.delegate = self
}
func updateWithModel(model: MyModel) {
/* update labels, images etc in this cell with data from model */
nameTextField.text = model.name
formulaTextField.text = model.formula
self.model = model
}
/* This code only works if MyModel is a class, because classes uses reference type, and the value
of the name and formula properies are changed in the model stored in the dictionary */
func textFieldShouldEndEditing(textField: UITextField) -> Bool {
let newText = textField.text
switch textField {
case nameTextField:
model?.name = newText
case formulaTextField:
model?.formula = newText
default:
print("Needed by compiler..")
}
}
}
class MyController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableVieW: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
/* This is not needed if you are using prototype cells in the Xib or Storyboard.
Convenient to use nib name as cell identifier */
tableVieW.registerNib(UINib(nibName: MyCustomCell.nibName, bundle: nil), forCellReuseIdentifier: MyCustomCell.nibName)
tableVieW.delegate = self
tableVieW.dataSource = self
}
private var dictionaryWithModelsForSection: Dictionary<Int, [MyModel]>!
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
let sectionCount = dictionaryWithModelsForSection.keys.count
return sectionCount
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let models: [MyModel] = modelsForSection(section) else {
return 0
}
let rowCount = models.count
return rowCount
}
private func modelsForSection(section: Int) -> [MyModel]? {
guard section < dictionaryWithModelsForSection.count else {
return nil
}
let models = dictionaryWithModelsForSection[section]
return models
}
private func modelAtIndexPath(indexPath: NSIndexPath) -> MyModel? {
guard let models = modelsForSection(indexPath.section) where models.count > indexPath.row else {
return nil
}
let model = models[indexPath.row]
return model
}
func addRowAtIndexPath(indexPath: NSIndexPath, withModel model: MyModel) {
add(model: model, atIndexPath: indexPath)
tableVieW.insertRowsAtIndexPaths([indexPath], withRowAnimation: .None)
}
private func add(model model: MyModel, atIndexPath indexPath: NSIndexPath) {
guard var models = modelsForSection(indexPath.section) where indexPath.row <= models.count else { return }
models.insert(model, atIndex: indexPath.row)
dictionaryWithModelsForSection[indexPath.section] = models
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(MyCustomCell.nibName, forIndexPath: indexPath)
return cell
}
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
guard let
cell = cell as? MyCustomCell,
model = modelAtIndexPath(indexPath) else { return }
cell.updateWithModel(model)
}
}
If you want to insert a cell you can use the method addRowAtIndexPath:withModel i wrote in MyController above, you need to call that from some function creating the corresponding model...

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.

Moving UITableViewCell between Two Section ios Swift

I am using two section like,
Section 1 with
cell 0
cell 1
cell 3 and
Section 2 with
cell 0
cell 1
cell 3
but i want to move cell 0 of section 2 in section 1
can any one explain me with code using swift programming
Basically, you need to understand that the data source needs to be updated before the cells get swapped otherwise you have a crash.
Look at the following example:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var data : [[String]] = [["Mike", "John", "Jane"], ["Phil", "Tania", "Monica"]]
#IBOutlet weak var tableView: UITableView!
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return data.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data[section].count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as UITableViewCell
let name = data[indexPath.section][indexPath.row]
(cell.viewWithTag(22) as UILabel).text = "(" + String(indexPath.section) + ":" + String(indexPath.item) + ") " + name
return cell
}
#IBAction func pressed(sender: UIButton) {
// arbitrarily define two indexPaths for testing purposes
let fromIndexPath = NSIndexPath(forRow: 0, inSection: 0)
let toIndexPath = NSIndexPath(forRow: 0, inSection: 1)
// swap the data between the 2 (internal) arrays
let dataPiece = data[fromIndexPath.section][fromIndexPath.row]
data[toIndexPath.section].insert(dataPiece, atIndex: toIndexPath.row)
data[fromIndexPath.section].removeAtIndex(fromIndexPath.row)
// Do the move between the table view rows
self.tableView.moveRowAtIndexPath(fromIndexPath, toIndexPath: toIndexPath)
}
}
Here I have the simplest case of a 2-d Array which holds some names. I define it as [[String]] to avoid casting it later. Before the In my Storyboard I have a button which calls 'pressed'. I swap the data source and then call moveRowAtIndexPath.
Try to use:
tableView.moveRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 2), toIndexPath: NSIndexPath(forRow: 0, inSection: 1))
Just don't forget to update your data model before calling this method.

Resources