I'm having problems with my app. I have a table view where every cell consists of a textfield. When i write in it and scroll down, than scroll back up, the data i wrote in it disappears.
These are some of my functions in ViewController
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, UIScrollViewDelegate {
var arrayOfNames : [String] = [String]()
var rowBeingEdited : Int? = nil
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return initialNumberOfRows
}
var count: Int = 0;
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TableViewCell
if(arrayOfNames.count > 0 && count < arrayOfNames.count) {
cell.TextField.text = self.arrayOfNames[indexPath.row]
}else{
cell.TextField.text = ""
}
count += 1
cell.TextField.tag = indexPath.row
cell.TextField.delegate = self
return cell
}
func textFieldDidEndEditing(_ textField: UITextField) {
let row = textField.tag
if row >= arrayOfNames.count {
for _ in ((arrayOfNames.count)..<row+1) {
arrayOfNames.append("") // this adds blank rows in case the user skips rows
}
}
arrayOfNames[row] = textField.text!
rowBeingEdited = nil
}
func textFieldDidBeginEditing(_ textField: UITextField) {
rowBeingEdited = textField.tag
}
}
As you can see, I'm saving all of my written text in the textfield into an array. Any help would be appreciated.
Thanks in advance
When you scroll back up tableView(tableView: cellForRowAt:) gets called again. Inside that method you increment count every time that is called, thus instead of using the first condition it goes to the second conditional statement that sets cell.TextField.text = "" as count is probably greater than arrayOfNames.count. What are you using count for? Maybe rethink how you could code that part a little better.
You cells are recreated. So you lose them. You could use the method PrepareForReuse to set the text back when they are recreated.
Related
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
I have spent days on resolving this issue and after trying much I am asking a question here. I am using a custom UITableViewCell and that cell contains UITextFields. On adding new cells to the table view, the table view behaves abnormal like it duplicates the cell and when I try to edit the textfield of new cell, the textfield of previous cel gets edited too.
The behavior of duplication is as follows: 1st cell is duplicated for 3rd cell. I don't know this is due to reusability of cells but could anyone tell me about the efficient solution?
I am attaching the screenshot of UITableViewCell.
The code for cellForRow is as follows:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : Product_PriceTableViewCell = tableView.dequeueReusableCell(withIdentifier: "product_priceCell") as! Product_PriceTableViewCell
cell.dropDownViewProducts.index = indexPath.row
cell.txtDescription.index = indexPath.row
cell.tfPrice.index = indexPath.row
cell.dropDownQty.index = indexPath.row
cell.tfTotalPrice_Euro.index = indexPath.row
cell.tfTotalPrice_IDR.index = indexPath.row
cell.dropDownViewTotalDiscount.index = indexPath.row
cell.dropDownViewDeposit.index = indexPath.row
cell.tfTotalDeposit_Euro.index = indexPath.row
cell.tfRemaingAfterDeposit_IDR.index = indexPath.row
return cell
}
The issue is the cell is being reused by the UITableView, which is what you want to happen for good scrolling performance.
You should update the data source that supports each row in the table to hold the text the user inputs in the field.
Then have the text field's text property assigned from your data source in cellForRowAt.
In other words, the UITableViewCell is the same instance each time you see it on the screen, and so is the UITextField and therefore so is it's text property. Which means it needs to be assigned it's correct text value each time cellForRowAt is called.
I'm unsure of your code so I have provided an example of how I would do something like what you want:
class MyCell: UITableViewCell {
#IBOutlet weak var inputField: UITextField!
}
class ViewController: UIViewController {
#IBOutlet weak var table: UITableView!
var items = [String]()
fileprivate func setupItems() {
items = ["Duck",
"Cow",
"Deer",
"Potato"
]
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
setupItems()
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// the # of rows will equal the # of items
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// we use the cell's indexPath.row to
// to get the item in the array's text
// and use it as the cell's input field text
guard let cell = tableView.dequeueReusableCell(withIdentifier: "myCell") as? MyCell else {
return UITableViewCell()
}
// now even if the cell is the same instance
// it's field's text is assigned each time
cell.inputField.text = items[indexPath.row]
// Use the tag on UITextField
// to track the indexPath.row that
// it's current being presented for
cell.inputField.tag = indexPath.row
// become the field's delegate
cell.inputField.delegate = self
return cell
}
}
extension ViewController: UITextFieldDelegate {
// or whatever method(s) matches the app's
// input style for this view
func textFieldDidEndEditing(_ textField: UITextField) {
guard let text = textField.text else {
return // nothing to update
}
// use the field's tag
// to update the correct element
items[textField.tag] = text
}
}
I suggest to do the following
class Product_PriceTableViewCell: UITableViewCell {
var indexRow: Int = -1
func configureCell(index: Int) {
cell.dropDownViewProducts.clean()
...
cell.tfRemaingAfterDeposit_IDR.clean()
}
}
where clean is the function to empty de view (depend on the type)
Then in the delegate:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : Product_PriceTableViewCell = tableView.dequeueReusableCell(withIdentifier: "product_priceCell") as! Product_PriceTableViewCell
cell.configureCell(row: indexPath.row)
return cell
}
As #thefredelement pointed out when the cell is not in the view frame, it is not created. Only when the view is going to appear, it tries to reuse an instance of the cell and as the first is available, the table view uses it but does not reinitialize it. So you have to make sure to clean the data
The rest of the answer is for better coding.
I'm a beginning iOS developer and I've been stuck on an issue for quite some time now.
Background:
I have a single viewcontroller in which I have placed a TableView (I cannot use a tableviewcontroller). It holds 4 dynamic prototype cells:
Cell 1 has an UITextField and a couple of labels, Cell 2-4 only have a label (with different types of information) and have to be hidden initially.
When an user enters a number (max. 3 digits) in the UITextField of the first cell, the number has to be compared to check if it is a correct/existing number. If this number proves correct, 2 things will have to happen: 1) the labels in the first cell will need to change layout (colour, font, size, ...) and set the data of one label. 2) the other cells in the tableview will have to appear beneath the first cell.
Problem:
I'm having trouble sending data back and forth between the first UITableViewCell and its UITableView. For the text input I use the shouldChangeCharactersInRange method in the cell class, which then limits the # of digits and calls a method in the tableView class where the data will be compared. For this I used delegation.
However, after checking whether the number is a match, I need to call a method in the cell class (from within the tableview class) that will change the layout and set the data of a label in the cell. Yet I can't seem to figure out a way to make this method call work and access the outlets.
Summarised: cell class sends number to tableview class, methods in tableview class run, tableview class sends bool to cell class where outlets need to be changed.
What I tried:
I tried setting up delegation in the other direction, but it wouldn't trigger. Using a normal method call wouldn't work either, because then the outlets are nil.
I believe my problem lies in the fact that I need to reference the same instance/object of the cell to access the outlets?
I selected and simplified the relevant pieces of code:
1) TableView class
class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, updateUITableView {
var existingNumber = 200
var cellsRowHeight = false
#IBOutlet var tableView: UITableView!
//Started by InputTableViewCell when UITextField has a 3 digit-number
func checkNumberInput(inputNumber: Int) {
//Step 1: check if the number matches an existing one
let match = checkNumberMatch(inputNumber: inputNumber)
//Step 2: send a bool back to the cell class to change the layout through outlets
InputTableViewCell().changeLayout(numberMatch: match) // <--- problem
//Step 3: make the hidden cells appear
toggleCellsVisibility(numberMatch: match)
}
//Step 1
func checkNumberMatch(inputNumber: Int) -> Bool {
if inputNumber == existingNumber {
return true
} else {
return false
}
}
//Step 2
func toggleCellsVisibility(numberMatch: Bool) {
cellsRowHeight = numberMatch
if numberMatch == true { //cells appear
tableView?.beginUpdates()
tableView?.endUpdates()
//possible additional code
} else { //cells dissappear
tableView?.beginUpdates()
tableView?.endUpdates()
//possible additional code
}
}
//Step 3
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
switch (indexPath.row) {
case 0 where !cellsRowHeight || cellsRowHeight:
return 170
case 1 where !cellsRowHeight:
return 0
case 1 where cellsRowHeight:
return 54
//cases for other cells to appear/dissappear
default:
return 44
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
//Create tableViewCell
let cellIdentifier = "InputCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! InputTableViewCell
cell.delegate = self
//Customize lay-out of cell
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsets.zero
cell.layoutMargins = UIEdgeInsets.zero
return cell
}
//creation of other cells
}
// MARK: - Loading
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView(frame: CGRect.zero)
}}
2) Cell class:
protocol updateUITableView: class {
func checkNumberInput(inputNumber: Int)
}
class InputTableViewCell: UITableViewCell, UITextFieldDelegate {
var delegate: updateUITableView?
#IBOutlet var textField: UITextField!
#IBOutlet var letterLabel: UILabel!
//Step 2: problem!
func changeLayout(numberMatch: Bool) {
if numberMatch == true {
print("Success") //this line triggers
letterLabel?.text = "A" //is nil
//other lay-out changes
}
else {
print("Fail, please provide an existing number")
//other lay-out changes
}
}
//Set maximum character limit in textField and dismiss keyboard when character limit is reached.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let currentCharacterCount = textField.text?.characters.count ?? 0
let newLength = currentCharacterCount + string.characters.count - range.length
if (newLength == 3) {
textField.text = (textField.text! as NSString).replacingCharacters(in: range, with: string)
//Get text from textField
let numberInput: Int? = Int(textField.text!)
if numberInput != nil {
delegate?.checkNumberInput(number: numberInput!) //send number to tableview class
}
//Dismiss keyboard
textField.resignFirstResponder()
if (range.length + range.location > currentCharacterCount) {
return false
} else {
return true
}
}
return true
}
func viewDidLoad() {
}}
The actual problem is situated in "Step 2". Using this method I can perform the print statement, but the actual labels/outlets are nil because it is just a generic call.
Any help would be immensely appreciated! Thank you!
Pass your UITableViewCell instance in your delegate method like this
func checkNumberInput(senderCell: InputTableViewCell,inputNumber: Int)
and then you will be able to call any method on this cell's instance. So in your case.
func checkNumberInput(senderCell: InputTableViewCell,inputNumber: Int){
//...
senderCell.changeLayout(numberMatch: match)
//....
}
and you can call your method like
delegate?.checkNumberInput(senderCell: self, inputNumber: numberInput!)
You can also pass data using NSNotificationCenter.
Put this code in viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(receivedDataFromNotification(notification:)), name: NSNotification.Name(rawValue: "receiveData"), object: nil)
This selector method for notification
func receivedDataFromNotification(notification : NSNotification) -> Void {}
Then Post Notification while you need to pass data.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "receiveData"), object: YourObject)
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...
I have a UICollectionView containing a matrix of text fields. Since the number of text fields per row can be high, I implemented my custom UICollectionViewLayout to allow scrolling in this screen. When the user submits the form, I want to validate the value entered in every text field, thus I need to loop all the cells.
The problem that I'm facing is that I was using collectionView.cellForItemAtIndexPath for this but then found out that it fails with invisible cells, as I saw on this this question.
I understand the approach in the answer to store the values of the data source (in arrays) and then to loop the data source instead, however I don't know how to do this. I tried using function editingDidEnd as an #IBAction associated to the text field but I don't know how to get the "coordinates" of that text field. My idea behind this is to store the value just entered by the user in a two-dimensions array that I'll use later on to loop and validate.
Many thanks for your help in advance!
You don't have to loop invisible cells. Keep using datasource approach. What you are looking for is the way to map textFields to the datasource.
There are many solutions, but the easy one is using Dictionary.
Here's the code for UITableViewDataSource but you can apply it to UICollectionViewDataSource
class MyCustomCell: UITableViewCell{
#IBOutlet weak var textField: UITextField!
}
class ViewController: UIViewController{
// datasource
var textSections = [ [ "one" , "two" , "three"] , [ "1" , "2" , "3"] ]
// textField to datasource mapping
var textFieldMap: [UITextField:NSIndexPath] = [:]
// MARK: - Action
func textChanged(sender: UITextField){
guard let indexPath = textFieldMap[sender] else { return }
guard let textFieldText = sender.text else { return }
textSections[indexPath.section][indexPath.row] = textFieldText
}
#IBAction func submitButtonTapped(){
// validate texts here
for textRow in textSections{
for text in textRow{
if text.characters.count <= 0{
return print("length must be > 0.")
}
}
}
performSegueWithIdentifier("goToNextPage", sender: self)
}
}
extension ViewController: UITableViewDataSource{
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("identifer") as! MyCustomCell
// set value corresponds to your datasource
cell.textField.text = textSections[indexPath.section][indexPath.row]
// set mapping
textFieldMap[cell.textField] = indexPath
// add action-target to textfield
cell.textField.addTarget(self, action: "textChanged:", forControlEvents: .EditingChanged)
return cell
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return textSections[section].count
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return textSections.count
}
}