How to style a UITableViewCell different? - ios

I'm trying to set a different style for my active object in a TableView. I tried setting a flag for my object (myObject.isActive) and read it in my custom UITableViewCell like this;
var myArray = [MyObject]()
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "myCustomCell", for: indexPath) as? myCustomCell {
if myArray.count > 0 {
// Return the cell
let myObject = myArray[indexPath.row]
cell.updateUI(myObject: myObject)
return cell
}
}
return UITableViewCell()
}
myCustomCell:
func updateUI(myObject: MyObject){
if myObject.isActive {
self.selectedCell()
}
}
func selectedCell() {
labelTitle.font = UIFont(name: "Montserrat-Medium", size: 32)
labelTitle.textColor = UIColor(hex: 0x64BA00)
}
This works great when the tableView data loads. But when I scroll the tableView other cells are also styling differently. How can I solve this?

Cells get reused. You need to handle all possibilities. Your updateUI method changes the cell if myObject is active but you make no attempt to reset the cell if it isn't.
You need something like:
func updateUI(myObject: MyObject){
if myObject.isActive {
selectedCell()
} else {
resetCell()
}
}
And add:
func resetCell() {
// Set the cell's UI as needed
}
Another options is to override the prepareForReuse method of the table cell class. That method should reset the cell to its initial state.

Related

Swift 4 UITableView make one cell as UITextView

I have a dynamic Table as UITableView and all cells are as normal (retrieved form my array)
However I need only one cell as TextView to be able input text. On text Change I need to retrieve the text user input.
How to make this?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count+1 //to allow this input field
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(indexPath.row < array.cont){
//normal cell from array
let cell = Table.dequeueReusableCell(withIdentifier: "myCell")
cell?.textLabel?.text = array[indexPath.row]
cell?.isUserInteractionEnabled = true;
cell?.textLabel?.adjustsFontSizeToFitWidth = true
cell?.textLabel?.textAlignment = .center;
return cell!;
}else{
//create input text field (DON'T KNOW HOW)
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if(indexPath.row < array.cont){
//.. perform action ..
}else{
//retrieve input text (DONT know How)
}
}
Creating UITextView inside UITableViewCell is quite simple :
let textView: UITextView = UITextView(frame: CGRect(x: 0, y: 20, width: 311.00, height: 50.00)) // Set frames as per requirements
textView.textAlignment = NSTextAlignment.justified
cell.contentView.addSubView(textView)
But this would lead to incorrect values while scrolling the table. Best approach would be to create a custom cell and add UITextView there. Here is the custom cell. Keep the constraints intact.
Before using the custom cell, you need to register it in your table. So :
let nib = UINib(nibName: "TextCell", bundle: nil)
Table.register(nib, forCellReuseIdentifier: "TextCell")
Don't forget to put identifier of cell in xib.
Now in cellForRowAtIndexPath :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(indexPath.row < array.cont){
//normal cell from array
let cell = Table.dequeueReusableCell(withIdentifier: "myCell")
cell?.textLabel?.text = array[indexPath.row]
cell?.isUserInteractionEnabled = true;
cell?.textLabel?.adjustsFontSizeToFitWidth = true
cell?.textLabel?.textAlignment = .center;
return cell!;
}else{
//create input text field (DON'T KNOW HOW)
let cell = tableView.dequeueReusableCell(withIdentifier: "TextCell", for: indexPath) as! TextCell
// Access UITextView by cell.textView
return cell
}
}
The main issue is - dynamic cell size as per UITextView height. But that entirely depends on your requirement. You can follow this post for that.
You can achieve this with delegation pattern or NSNotification.
Here's the solution for this using delegation pattern.
Create new UITableViewCell using xib and add the textView on contentView of cell, set the reuse identifier and than register the xib in the ViewController with
tableView.register(UINib(nibName: "name of the xib file", bundle: nil), forCellReuseIdentifier: "Identifier here")
Now define protocol anywhere outside of the class
// You can give any name
// Here we are confirming to class to avoid any retain cycles
protocol CustomCellDelegate :class {
func returnText(text :String)
}
Now initialise " var delegate : CustomCellDelegate? " in same class of UITableViewCell that we created above while creating xib.
and confirm to protocol UITextViewDelegate and than in the cell class write this
override func awakeFromNib() {
super.awakeFromNib()
textView.delegate = self
}
after that add these functions in same class of tableViewCell
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if(text == "\n") {
textView.resignFirstResponder()
return false
}
return true
}
func textViewDidEndEditing(_ textView: UITextView) {
delegate.returnText(text : textView.text ?? "")
}
Now in the ViewController class
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == array.count { // this will call the cell with TextView at the end , you can play with any indexPath to call the cell
let cell = tableView.de... // Deque the cell with TextView here using reuse identifier
cell.delegate = self
return cell
}
else {
// deque other cells
}
}
we'll write an extension of ViewController and confirm to our custom protocol
extension ViewController : CustomCellDelegate {
// this function will get called when you end editing on textView
func returnText(text :String) {
print(text) // you may save this string in any variable in ViewController class
}
}
Add a TextView in to your custom cell, hide it, show when you need
if you want to have the textView on the top of all cells, drag and drop a UIView inside the tableView before the cell.
this view will scroll with cells.
design this view as you need insert a textView inside it, and use textView's delegate methods to perform operations.

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.

Iterating through TableViewCells skips some

I have a tableview and in each cell there is a checkbox. I also have a "select all" button.
My problem is that when I click select all I want to update all the checkboxes to checked state. So from a list of 100 cells, all get checked but every 13th cell does not. To make it clearer, on my simulators screen are 12 cells visible that all get checked. When I start scrolling, the first cell that comes up is unchecked, and is then followed by 12 checked ones :S
When I scroll a little and click "select all" again, the skipped ones become also checked..
Anyone have a clue what am I missing?
This is the cell code:
class ListTableViewCell: UITableViewCell {
#IBOutlet weak var checkbox: UIButton!
var buttonState = false{
didSet{
if buttonState{
checkbox.setImage(#imageLiteral(resourceName: "checked"), for: .normal)
}else{
checkbox.setImage(#imageLiteral(resourceName: "unchecked"), for: .normal)
}
}
}
#IBAction func checkboxAction(_ sender: UIButton) {
if buttonState {
buttonState = false
}else{
buttonState = true
}
}
func simulateCheck(){
buttonState = true
}
And here are some snipets from my controller:
private var articleValues: [ArticleValue] = []{
didSet{
tableView.reloadData()
}
}
func selectAll(){
for i in 0..<articleValues.count{
let cell = tableView.cellForRow(at: IndexPath(item: i, section: 0)) as? ListTableViewCell
cell?.simulateCheck()
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
}
return cell
}
Your UITableView is backed by a data source. This means that you shouldn't change cells directly like you do here:
cell?.simulateCheck()
tableView.reloadData()
Instead you should keep a list of all the checked positions, maybe another array that has bools for each corresponding articleValue (this is not the best design).
var checkedValues = Bool
In your
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell method you would then set the state of the cell:
articleValueCell.buttonState = checkedValues[indexPath.row]
In your selectAll method fill this array with true values and then call tableView.reloadData()
private var checkedValues = [Bool]()
private var articleValues: [ArticleValue] = []{
didSet{
checkedValues = Array(repeating: false, count: articleValues.count)
tableView.reloadData()
}
}
func selectAll(){
checkedValues = Array(repeating: true, count: articleValues.count)
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
articleValueCell.buttonState = checkedValues[indexPath.row]
}
return cell
}
Another mistake is that you should never iterate on all the cells in the table because they are reused, no point in going through your data source and getting a cell for each. It only makes sense to iterate through tableView.visibleCells. But like in your case, most of the time you don't need that either, you should just update your data source accordingly and reload the table or just the modified cell.
It's not recommended that you refer to cells directly within a table view. The reason is that UITableViews have an efficient method of only loading the cells as they are needed (and deallocating them when they are no longer needed, e.g. the cell scrolls off screen). Because of this the cell you are try to refer to may not be loaded.
Instead you should interact with it via the cellForRowAt method. If you want to "select all" cells, you should create a property that stores the value of checked or not checked via a Bool and then set all of the ArticleValue elements to true for that property and reload the data inside selectAll().
It could work something like this:
func selectAll() {
articleValues.forEach {
$0.checked = true
}
tableView.reloadData()
}
// ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "articleValueItem", for: indexPath)
// Cell Configuration
let articleValue = articleValues[indexPath.row]
if let articleValueCell = cell as? ListTableViewCell{
articleValueCell.articleValue = articleValue
if articleValue.checked {
articleValueCell.simulateCheck()
}
}
return cell
}

tableView.cellForRowAtIndexPath returns nil with too many cells (swift)

So I have the weirdest thing;
I am looping a tableView in order to iterate over all cells. It works fine with less than 5 cells, but crashes with "unexpectedly found nil" for more cells. Here's the code:
for section in 0..<tableView.numberOfSections {
for row in 0..<tableView.numberofRowsInSection(section) {
let indexPath = NSIndexPath(forRow: row, inSection: section)
let cell = tableView?.cellForRowAtIndexPath(indexPath) as? MenuItemTableViewCell
// extract cell properties
The last line is the one that gives the error.
Any thoughts?
Because cells are reused, cellForRowAtIndexPath will give you cell only if cell for given indexPath is currently visible. It is indicated by the optional value. If you want to prevent from crash, you should use if let
if let cell = tableView?.cellForRowAtIndexPath(indexPath) as? MenuItemTableViewCell {
// Do something with cell
}
If you want to update values from cell, your cells should update the dataSource items. For example you can create delegate for that
protocol UITableViewCellUpdateDelegate {
func cellDidChangeValue(cell: UITableViewCell)
}
Add delegate to your cell and suppose we have a textField in this cell. We add target for the didCHangeTextFieldValue: for EditingDidChange event so it is called every time the user types somethink in it. And when he do, we call the delegate function.
class MyCell: UITableViewCell {
#IBOutlet var textField: UITextField!
var delegate: UITableViewCellUpdateDelegate?
override func awakeFromNib() {
textField.addTarget(self, action: Selector("didCHangeTextFieldValue:"), forControlEvents: UIControlEvents.EditingChanged)
}
#IBAction func didCHangeTextFieldValue(sender: AnyObject?) {
self.delegate?.cellDidChangeValue(cell)
}
}
Then in cellForRowAtIndexPath you add the delegate
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellIdentifier", forIndexPath: indexPath)
cell.delegate = self
return cell
}
And finally we implement the delegate method:
func cellDidChangeValue(cell: UITableViewCell) {
guard let indexPath = self.tableView.indexPathForCell(cell) else {
return
}
/// Update data source - we have cell and its indexPath
}
Hope it helps

Custom Cell for 2 TableViews/Segmented Control Swift

So I have a Segmented Control that switches between 2 TableViews & 1 MapView inside a MainVC.
I'm able to switch the views in the simulator by adding an IBAction func changedInSegmentedControl to switch which views are hidden while one of them is active.
I created 2 custom TableViewCells with XIBs. I also added tags with each TableView.
My question is how do I add them in cellForRowAtIndexPath?
Currently, my code is:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
// var cell: UITableViewCell
if (tableView.tag == 1) {
cell: CardTableViewCell = tableView.dequeueReusableCellWithIdentifier("CardCell") as! CardTableViewCell
return cell
}
else if (tableView.tag == 2) {
cell: ListTableViewCell = tableView.dequeueReusableCellWithIdentifier("ListCell") as! ListTableViewCell
return cell
}
}
Of course Swift requires a "return cell" for the function outside the If statements. I tried with a var cell: UITableViewCell outside, but run into trouble finishing the dequeuReusableCellWithIdentifier.
Anyone have some idea how to do this? Thanks.
This is how I approached it (Swift 3.0 on iOS 10). I made one tableView with two custom cells (each is their own subclass). The segmented control is on my navigationBar and has two segments: People and Places.
There are two arrays within my class, (people and places) which are the data sources for the two table views. An action attached to the segmentedControl triggers the reload of the table, and the switch statement in cellForRowAtIndex controls which cell from which array is loaded.
I load data into the two data arrays from an API call, the asynchronous completion of which triggers dataLoaded(), which reloads the tableView. Again I don't have to worry about which segment is selected when the table is reloaded: cellForRowAtIndex takes care of loading the correct data.
I initialize a basic cell just as UITableViewCell and then in the case statement I created and configure the custom cell. Then I return my custom type cell at the end, and as long as the reuseIdentifiers and classes are correct in cellForRowAtIndex, the correct cell is initialized and displayed in the tableView.
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var peoplePlacesControl: UISegmentedControl!
fileprivate var placesArray: [PlaceModel]?
fileprivate var usersArray: [UserModel]?
#IBAction func segmentedControlActionChanged(_ sender: AnyObject) {
tableView.reloadData()
switch segmentedControl.selectedSegmentIndex {
case 0:
loadUsersfromAPI()
case 1:
loadPlacesFromAPI()
default:
// shouldnt get here
return
}
}
func dataLoaded() {
switch peoplePlacesControl.selectedSegmentIndex {
case 0: // users
if favoriteUsersArray == nil {
self.tableView.reloadData()
} else {
hideTableViewWhileEmpty()
}
case 1: // places
if placesArray != nil {
self.tableView.reloadData()
} else {
hideTableViewWhileEmpty()
}
default:
return
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch segmentedControl.selectedSegmentIndex {
case 0:
if usersArray != nil {
return usersArray!.count
} else {
return 0
}
case 1: // places
if placesArray != nil {
return placesArray!.count
} else {
return 0
}
default:
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = UITableViewCell()
switch peoplePlacesControl.selectedSegmentIndex {
case 0: // people
let userCell = tableView.dequeueReusableCell(withIdentifier: "MyUserCell", for: indexPath) as! MyUserTableViewCell
if usersArray != nil && indexPath.row < usersArray!.count {
let user = usersArray![indexPath.row]
userCell.configure(user)
userCell.myDelegate = self
}
cell = userCell as MyUserTableViewCell
case 1: // places
let placeCell = tableView.dequeueReusableCell(withIdentifier: "MyPlaceCell", for: indexPath) as! MyPlaceTableViewCell
if favoritePlacesArray != nil && indexPath.row < favoritePlacesArray!.count {
let place = placesArray![indexPath.row]
placeCell.configure(place)
placeCell.myDelegate = self
}
cell = placeCell as MyPlaceTableViewCell
default:
break
}
return cell
}
I have made change in your code.
Use following code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
if (tableView.tag == 1) {
cell: CardTableViewCell = tableView.dequeueReusableCellWithIdentifier("CardCell") as! CardTableViewCell
return cell
}
cell: ListTableViewCell = tableView.dequeueReusableCellWithIdentifier("ListCell") as! ListTableViewCell
return cell
}

Resources