How to loop invisible cells in UICollectionView - ios

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
}
}

Related

Changing the value of one textfield changes a couple of other textfields as well, unintentionally. How do I prevent this?

I have a table view of custom table view cells. Each of the cells has a text field where I can fill in some numeric data. I have set up a delegate method i.e. textFieldDidEndEditing which after editing will add the value entered into a text field into a swift hashtable.
However, I see that some other text field which belongs to another completely different table view cell also now has the same value that I entered.
In order to solve this problem, I tried to add other delegate based text field methods thinking that they should solve the problem at hand. One of the methods I used was the textFieldDidChange method and in that method, I wrote the check that if the text field tag was not the same as the tag of the deliberately edited text field, then I clear the text field out.
func textFieldDidChange(_ textField: UITextField) {
if textField.tag != self.service!.id {
print("CLEARING AFFECTED TEXTFIELD")
textField.text = ""
}
}
I probably used the method the wrong way as it did not have any effect on the problem.
I am adding the code snippets which are involved in the problem at hand:-
BookingServiceChargeViewCell.swift
import UIKit
import PineKit
import SwiftMoment
class BookingServiceChargeViewCell: UITableViewCell, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
//Other variables
let price = TextField(placeholder: "Price")
//Other methods
func layoutContent() {
//function to set the layout of the cell
self.price.font = Config.Font.get(.Regular, size: 13)
self.price.delegate = self
self.price.setValue(UIColor.init(colorLiteralRed: 71/255, green: 72/255, blue: 73/255, alpha: 1.0), forKeyPath: "_placeholderLabel.textColor")
self.price.keyboardType = UIKeyboardType.numberPad
self.price.addDoneButtonOnKeyboard()
self.price.setBottomBorder()
self.price.snp.makeConstraints { (make) in
make.centerY.equalTo(cover)
make.width.equalTo(75)
make.right.equalTo(content.snp.right).offset(-40)
}
}
func configure(_ service: Service, subServices: [Service], index: Int, parentView: OnboardingChosenServicesViewController) {
self.service = service
self.subServices = subServices
self.itemIndex = index
self.parentView = parentView
if (self.service!.defaultImage != nil){
ImageLoader.sharedLoader.imageForUrl(urlString: self.service!.defaultImage!) { (image, url) in
self.cover.image = image
}
}
self.serviceName.text = self.service!.name!
self.price.tag = self.service!.id
self.table.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.subServices.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! BookingSubServicesChargeViewCell
cell.configure(self.subServices[indexPath.row], index: indexPath.row, parentView: self.parentView!)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
func textFieldDidEndEditing(_ textField: UITextField) {
self.parentView!.serviceAndCharges[textField.tag] = Int(textField.text!)
print(self.parentView!.serviceAndCharges)
}
}
I am uploading a couple of screenshots showing the problem:-
As you can see, I have entered a numeric value to the text field that is in the 'Daycare' cell
In this screenshot the text field in the 'Walking' cell should not have been edited at all, it should have remained blank like all other text fields except the one I just edited.
How can I solve this problem?
This problem arises when you dequeue your cell with the same identifier. To solve this problem, you have to save the values written in the cell in some data set and while reloading table view in cellForRow method, use that data set to configure your cell.
You can do that by making a delegate in your CustomCell and make your custom cell a delegate of the text field present in your cell and when text field did end editing you can pass those values to your controller via delegate and save those values in the datasource. While loading your tableview use that datasource to configure your cell

How to add multiple data to a UITableView from a UITextField? (SWIFT 4)

I am trying to create a program on Xcode that allows the user to enter multiple data into a table view through a text field (when a button is clicked). When the data is added I would like it to be stored and not be deleted after the app is closed - for this part I believe that I would have to use NSUserDefaults, however, I am unsure how I would save an array of strings? (I'm only familiar with storing a single string).
This is what my view controller currently looks like.
I have not done much on my view controller at all but this is what it currently has.
import UIKit
class NewViewController: UIViewController {
#IBOutlet weak var text: UITextField!
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
Let's tackle this step-by-step...
TL;DR - For your convenience, I've put the final code into a sample project on Github. Feel free to use any or all of the code in your apps. Best of luck!
Step 1 - Conform to UITableView Protocols
"...enter multiple data into a table view..."
At a minimum, UITableView requires you to conform to two protocols in order to display data: UITableViewDelegate and UITableViewDataSource. Interface Builder handles the protocol declaration for you if you use the built-in UITableViewController object, but in your case you cannot use that object because you only want the UITableView to take up a portion of the view. Therefore, you must implement the protocols yourself by adding them to ViewController's signature:
Swift 4
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
}
Step 2 - Implement UITableView Protocol Required Methods
Now that you have the protocols declared, Xcode displays an error until three required methods are implemented inside of your ViewController class. The bare minimum implementation for these methods is:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
You'll implement these methods later, but at this point your code should compile.
Step 3 - Connect UITableView's Protocols to ViewController
Since you are using a standard UITableView object, ViewController is not connected by default to the code you just implemented in the protocol methods. To make a connection, add these lines to viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
Alternatively, you could use the CONTROL + DRAG technique in Interface Builder to connect the delegate and data source from your UITableView to ViewController.
NOTE: In this case, self refers to the ViewController since you're inside of the ViewController class.
Step 4 - UITextField Setup
"...through a text field..."
You previously added an IBOutlet for your UITextField that is connected to Interface Builder, so there is nothing more to do here.
Step 5 - IBAction for the Add Button
(when a button is clicked)."
You need to add an IBAction to your ViewController class and connect it to your Add Button in Interface Builder. If you prefer to write code and then connect the action, then add this method to your ViewController:
#IBAction func addButtonPressed(_ sender: UIButton) {
}
If you use Interface Builder and the CONTROL + DRAG technique to connect the action, the method will be added automatically.
Step 6 - Add an Array Property to Store Data Entries
"...save an array of strings..."
You need an array of strings to store the user's entries. Add a property to ViewController that is initialized as an empty array of strings:
var dataArray = [String]()
Step 7 - Finish Implementing UITableView Protocol Methods
At this point you have everything you need to finish implementing UITableView's protocol methods. Change the code to the following:
//1
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//Do nothing
}
//2
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
//3
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = dataArray[indexPath.row]
return cell
}
In the future, if you want to do something when the user taps a cell, you will want to add code to tableView(_:didSelectRowAt:).
You now create the same number of rows as the number of values in dataArray.
To make this work with Interface Builder, make sure you go to the Attributes Inspector for your UITableViewCell and set the Cell Identifier to Cell. Check out the documentation for more on Dequeuing Cells.
Step 8 - Finish Implementing addButtonPressed(_:)
As suggested in #dani's answer, in the action you need to implement code that appends the user's text to the array, but only if the text is not blank or empty. It is also a good idea to check if dataArray already contains the value you entered using dataArray.contains, depending on what you want to accomplish:
if textField.text != "" && textField.text != nil {
let entry = textField.text!
if !dataArray.contains(entry) {
dataArray.append(entry)
textField.text = ""
}
tableView.reloadData()
}
Step 9 - Persist Data with UserDefaults
"When the data is added I would like it to be stored and not be deleted after the app is closed."
To save dataArray to UserDefaults, add this line of code after the line that appends an entry inside of the addButtonPressed(_:) action:
UserDefaults.standard.set(dataArray, forKey: "DataArray")
To load dataArray from UserDefaults, add these lines of code to viewDidLoad() after the call to super:
if let data = UserDefaults.standard.value(forKey: "DataArray") as? [String] {
dataArray = data
}
Try the following:
Create an array that will store all the text entered via the UITextField (ie. var array = [String]()
In the action of that add button, append the text the user has entered in the text field to the array.
if text.text != "" && !text.text.isEmpty {
// append the text to your array
array.append(text.text!)
text.text = "" // empty the `UITextField`
}
In your tableView methods, make the numberOfRows return array.count and just add a UILabel for your custom UITableViewCell that will display each entered item from the array in a separate cell.
if you want to display your data in tableview you need to implement tableview delegates. add a table view cell with a label on it
#IBOutlet weak var text: UITextField!
#IBOutlet weak var tableView: UITableView!
let NSUD_DATA = "dataarray_store"
var dataArray : NSMutableArray!
var userDefault = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
dataArray = NSMutableArray()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//MARK:- create a button for adding the strings to array and while clicking that button
func onClickButton(){
let string = text.text
dataArray.add(string)
userDefault.set(dataArray, forKey: NSUD_DATA)
}
for getting array stored in userdefault
func getData() -> NSMutableArray?{
if userDefault.object(forKey: NSUD_DATA) != nil{
return userDefault.array(forKey: NSUD_DATA) as! NSMutableArray
}
return nil
}
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var entertxt: UITextField!
#IBOutlet weak var save: UIButton!
#IBOutlet weak var tableview: UITableView!
var names = [String]()
override func viewDidLoad() {
super.viewDidLoad()
if let data = UserDefaults.standard.object(forKey: "todolist") as?[String]
{
names = data
}
}
#IBAction func submit(_ sender: Any) {
if entertxt.text != "" {
names.append(entertxt.text!)
UserDefaults.standard.set(names, forKey: "todolist")
tableview.reloadData()
entertxt.text = ""
}
else
{
print("data not found")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! myTableViewCell
cell.namelable.text = names[indexPath.row]
return cell
}

UITableView Duplicate cells (custom cells with textfields)

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.

UItextField data disappears when I scroll through the TableView -- Swift

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.

Reusing tableViewCell with UITextFields gives same data from an Array

I am having a section with custom tableViewCell and this cell has two text fields.
This cell is used to display phone number of a user - country code in one text field and the number in other text field.
I fetch data from a service and if the user has more than one phone, based on the count of the phones, I am updating numberOfRowsInSection and cellForRowAtIndexPath. I do not have any issue till here.
For example, if user has two phones, I return 2 in numberOfRowsInSection, and shows two cells (same custom cell with two textFields).
The Problem:
I get the same data - the second phone number details - in both the cells. But I need to have list of phone numbers displayed one after the other in my custom cell textfields.
Here is my code:
numberOfRowsInSection
public override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 3:
return (user.phones?.count)!
default:
return 1
}
}
cellForRowAtIndexPath
public override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
case .ContactPhones:
if contactCellExpanded == true {
cell = tableView.dequeueReusableCellWithIdentifier("PhoneCell") as? PhoneCell
}
case .ContactEmail:
if contactCellExpanded == true {
cell = tableView.dequeueReusableCellWithIdentifier("EmailCell") as? EmailCell
}
default:
break
}
PhoneCell
import UIKit
class PhoneCell: ProfileCell {
#IBOutlet weak var countryCode: UITextField!
#IBOutlet weak var addPhoneButton: UIButton!
#IBOutlet weak var teleNumber: UITextField!
#IBAction func addPhoneButtonAction(sender: AnyObject) {
}
override func configure(withUser user: XtraUser, language: String, indexPath: NSIndexPath) {
super.configure(withUser: user, language: language, indexPath: indexPath)
self.countryCode.borderStyle = .RoundedRect
self.teleNumber.borderStyle = .RoundedRect
if let userPhoneInfo = user.phones {
for i in 0..<userPhoneInfo.count {
print ("numbers \(userPhoneInfo[i].number)")
self.countryCode.text = userPhoneInfo[i].country
self.teleNumber.text = userPhoneInfo[i].number
}
}
}
}
I understand that I am using a for loop to get the numbers list when available, but how do I assign each value to the cell's textField?
Please Help!
Thanks in Advance
Since in configure method you know the index path of the cell why dont you just use
override func configure(withUser user: XtraUser, language: String, indexPath: NSIndexPath) {
super.configure(withUser: user, language: language, indexPath: indexPath)
self.countryCode.borderStyle = .RoundedRect
self.teleNumber.borderStyle = .RoundedRect
print("Enumerated \(user.phones?.enumerate())")
if let userPhoneInfo = user.phones {
self.countryCode.text = userPhoneInfo[indexPath.row].country
self.teleNumber.text = userPhoneInfo[indexPath.row].number
}
}
But anyway the best solution is to have model for each cell and pass it to this configure method.
Wrong logic implementation in numberOfRows method.
In that method set number of rows according to your data in response.

Resources