I use a UITableView inside a UIViewController with custom cells. However, when dequeueReusableCellWithIdentifier is called, it sometimes creates a new cell instead of reusing an existing one.
For example: When the first cell gets updated for the first time, a new cell is created instead of reusing the original cell.
I have created a diagram to help better illustrate the problem. Each green rectangle is an action that should happen, and each red rectangle is one that shouldn't. Each arrow is a call of dequeueReusableCellWithIdentifier.
It's problematic when a UIButton is tapped (the action is called multiple times), and with a "swipe to delete" action, the cell which has been swiped can be covered by the new second cell, which makes it appear like the delete button is randomly disappearing.
How can I solve this problem?
EDIT:
code custom cell
class TableViewCell : UITableViewCell{
#IBOutlet weak var buttonStop: UIButton!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("newCell")
backgroundColor = color
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
override func prepareForReuse() {
super.prepareForReuse()
}
var color = UIColor().randomColor()
}
code for extension randomColor
extension UIColor {
func randomColor() -> UIColor {
let aRedValue = CGFloat(arc4random()) % 255 / 255
let aGreenValue = CGFloat(arc4random()) % 255 / 255
let aBlueValue = CGFloat(arc4random()) % 255 / 255
return UIColor(red: aRedValue, green: aGreenValue, blue: aBlueValue, alpha: 1)
}
code for cellForRow
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCellWithIdentifier("Cell",forIndexPath: indexPath) as! TableViewCell
cell.buttonStop.tag = indexPath.row
cell.buttonStop.addTarget(self, action: "buttonAction:", forControlEvents: UIControlEvents.TouchUpInside)
return cell
}
buttonAction:
func buttonAction(sender: UIButton) {
print("action for cell : \(sender.tag)")
}
I reduced the code to the max , note after further investigation the button printed the same text because of one of my mistake I write the message twice ^^'
So the only problem remaining is for the "swipe to delete" gesture
If I understand correctly you have a problem with keeping (the result of) an action in sync with what is displayed in the tableView.
Understand how UITableView uses the cells: it will ask it's dataSource for a UITableViewCell. You can either create one every time, or ask the tableView for one to reuse. Because of this mechanism, a cell you create can appear in a certain position at one time, and another position later.
Make sure that your logic and data is independent of the actual tableViewCells at all times. So you should be able to do what you have to o with ONLY an NSIndexPath.
A UITableViewCell can give you a valid indexPath for a tableViewCell via one of two method -indexPathForCell: or indexPathForRowAtPoint:.
The way to handle actions on UITableViewCells is as follows:
Create a custom subclass of UITableViewCell
Create a protocol in this subclass (e.g. MyCellDelegate)
Give this class a property delegate of type id <MyCellDelegte>
When creating a cell in your dataSource, sign this dataSource to be the delegate of that cell.
Let this subclass handle the action (swipe / tap) by calling a method from your protocol on it's delegate (the delegate is your datasource).
In the implementation of that delegate method (on your datasource) you can call one of the methods on UITableView to get the indexPath. This is the point where you become independent on the actual tabelViewCell: use that indexPath to get data, or perform a certain operation.
Although this might seem like a lot of work for a small thing, it really is not that much work, and your code will be much more robust, easier to follow and less coupled.
Related
Current Setup
I have a custom cell, loaded from a xib, where most of its space is covered by UITextview. Also that cell may have a few textviews. And there are few more elements (one UIView + 2 UILabels) inside of this cell.
The Problem
I tried removing all those views and lag stays even if I have only one textview. Also, the lag is happening only for the first time. Later on, when I scroll down, and run into another cell with a textview in it, the lag doesn't happen at all.
Additional Info
The thing with this custom cell is that a textview is added to a UIStackView. At the beginning, a stackview is empty, because I don't know (at development time) how many textviews may/should be there.
I am aware that this is another thing that might affect on performance, but I have solved it ( I guess as best as it could) by checking how many textviews are already found in stackview's arrangedSubviews array when dequeuing a cell, and based on that info, I just add or hide views appropriately (rather than to destroying, and re-creating a required number of textviews each time).
I have tried using Instruments, but I didn't noticed that any of my classes take up CPU time, but rather some UIKit method calls that are called by the framework internally are the cause of this... If needed, I can post a screenshot, but I guess this is not relevant because those seem to be the usual system & framework calls. Plus I am testing on iPad 2 :D so maybe that is a thing (I have to optimize an app for slow devices).
Still, I guess I can optimize this somehow?
The MyCell class is rather simple (pseudo code):
class MyCell:UITableViewCell{
func configure(data:SomeData){
self.addOrHideViewsIfNeeded()
}
private func addOrHideViewsIfNeeded(){
//here, I check if stackview.arrangedSubviews has, and how many subviews are there, and
//add / hide them appropriately, means if I have to add them, I load them from the nib, otherwise, I reuse views from by adding them/removing them from a pool.
}
}
Also the lag is more noticealbe in Debug version in compare to Release version, which make sense, but it is still noticeable.
You might have to check if you are re-using the cells or not.
You can reuse it as below:
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NAIndexPath *) indexPath{
static NSString *cellIdentifier = #"Mycell";
cell = [tableView dequeueCellWithIdentifier:cellIdentifier];
if(cell == nil)
cell = [[MyCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: cellIdentifier];
}
Ok this is a sketch of my idea to do the preloading in an invisible from user perspective tableview row zero.
class MyCell : UITableViewCell {
static var initiallyPreloaded : Bool = false
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
if !MyCell.initiallyPreloaded {
//Do the initial preloading setup by adding UITextView to self.contentView
MyCell.initiallyPreloaded = true
} else {
//Setup the regular cell content otherwise
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
class ViewController: UIViewController , UITableViewDelegate {
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell{
var cell: MyCell? = tableView.dequeueReusableCell(withIdentifier: "myCellIdentifier") as! MyCell?
if cell == nil {
cell = MyCell(style: .default, reuseIdentifier: "myCellIdentifier")
}
if indexPath.row == 0 {
//Display the initial cell in unnoticeable to user way (start with hidden/alpha zero)
//For this zero-th row the initial preloading should happen
}
return cell!
}
}
I have been stuck for hours on this issue. I have created a custom cell inside a tableview which is inside a UIVC. The custom cell has it's own class file and is all linked up as well as having the buttons inside it connected to the UIVC via delegates.
Inside the custom cell there consists of a UITextView as well as some buttons.
Tapping the tableview adds a new custom cell. The user can type whatever their beautiful heart desires, eventually resigning the first responder of the textview. Here is my issue(s).
MAIN. First off. Overtime, the cells start mixing up the text that was typed in by the user, and starts reusing it. It becomes a mess.
Due to the fact I have a custom cell, I do not need to register the class in the viewDidLoad of the UIVC. A stack overflow answer stated.
Optional. Second. Eventually, after a certain point the keyboard blocks the cell view. I have no idea how to scroll the cell to the top of the tableview.
Optional. Third. This is just a bonus. How would one be able to save the data from a cell with a decent storage method. I heard NSUserDefaults is only good for small memory storage files. Basically. Transferring the data in the textview inside the custom cell, into another area (UIVC?) where the data can be saved. Most likely using an array and saving the data on that array.
Code. As requested.
UIVC
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let reminderCell = reminder_tableView.dequeueReusableCellWithIdentifier("cellReminder", forIndexPath:indexPath) as! addReminderCell
reminderCell.selectionStyle = .None
reminderCell.heightForReminderTextDelegate = self
reminderCell.delegate = self
reminderCell.reminder_textview.becomeFirstResponder()
return reminderCell
}
Custom Cell Code. Not all of it though.
import UIKit
import AVFoundation
//MARK: Height For Reminder Delegate
protocol HeightForReminderTextView
{
func heightOfTextView(height: CGFloat)
}
//MARK: Beginning of Class
class addReminderCell: UITableViewCell, UITextViewDelegate {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: nil)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
//MARK: Delegate Declarations
var heightForReminderTextDelegate :HeightForReminderTextView?
//MARK: Element IBOutlets
#IBOutlet var reminder_textview: UITextView!
#IBOutlet var reminder_cell_uiview: UIView!
//MARK: Button
#IBAction func non_priority_button(sender: AnyObject) {
println("pressed")
}
//MARK: Awake From Nib Starts Here
override func awakeFromNib() {
super.awakeFromNib()
reminder_textview.delegate = self
}
MAIN: When you dequeue a cell it means that cell may be reused it does not create a new cell everytime. For that UITableViewCell has a method named prepareForReuse() which you can override, where you can reset all your cell content. Ex:
override func prepareForReuse() {
myTextView.text = ""
}
Due to the fact I have a custom cell...
If you have only *.swift file for your custom cell you must registerClass(:forCellReuseIdentifier:)
If you also have *.xib file you should registerNib(:forCellReuseIdentifier:) in the viewDidLoad
Optional
You can use scrollToRowAtIndexPath(_:atScrollPosition:animated:) to scroll desired cell to top of tableView
Optional
You can use CoreData, JSON, some SQLite wrappers to save data. In NSUserDefaults you can use small amount of data, like some settings.
I recommend to use NSJSONSerialization for now. You can create NSData from a Dictionary and write data as *.json to Documents directory using NSFileManager. In your app you should keep an array or dictionary with all the data and pass to another view controller that data.
when user types text, you should save it to some array or dictionary accoding to your requirment and that would helpyou with resusing problem.
Now, second problem of yours is scrolling I answered this question hope this will help. this is in objective C although.
and third for transferring data from OneVc to another you can use Segues
Hope this helps
I have an uitableview with a custom cell which gets data from the array.
Custom cell has an uilabel and an uibutton (which is not visible until the uilabel text or the array object which loads for the text - is nil).
On launch everything is fine. When i press the uibutton the array is being appended, the new cells are being inserted below the cell.
But when i scroll - all of a sudden the uibutton appears on other cells where this conditional uilabel text isEmpty is not implied.
Here is how the whole process looks like
Here is my code for cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:TblCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! TblCell
cell.lblCarName.text = someTagsArray[indexPath.row]
if let text = cell.lblCarName.text where text.isEmpty {
cell.act1.hidden = false
} else {
println("Failed")
}
cell.act1.setTitle(answersdict[answersdict.endIndex - 2], forState:UIControlState.Normal)
cell.act2.setTitle(answersdict.last, forState:UIControlState.Normal)
return cell
}
So my general question is how do i stop the reuse of those custom cells?
As far as i'm aware there is no direct way of doing this on reusablecellswithidentifier in swift, but maybe there are some workarounds on that issue?
When a cell is reused, it still has the old values from its previous use.
You have to prepare it for reuse by resetting that flag which showed your hidden control.
You can do this either in tableView:cellForRowAtIndexPath: or the cell's prepareForReuse method.
Update:
Here's an example you can add for TblCell:
override func prepareForReuse()
{
super.prepareForReuse()
// Reset the cell for new row's data
self.act1.hidden = true
}
I'm creating a productivity app in Swift. I'm not using a prototype cell in the Storyboard as most of it has been written in code already. I'd like to a checkbox button.
How would I go about doing that?
While the answer from Tim is technically correct, I would not advise on doing this. Because the UITableView uses a dequeuing mechanism, you could actually receive a reused cell which already has a button on it (because you added it earlier). So your code is actually adding a 2nd button to it (and a 3rd, 4th, etc).
What you want to do, is create a subclass from the UITableViewCell which adds a button to itself, while it is being instantiated. Then you can just dequeue that cell from your UITableView and it will automatically have your button on it, without the need to do it in the cellForRowAtIndexPath method.
Something like this:
class MyCustomCellWithButton: UITableViewCell {
var clickButton = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton;
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier);
self.contentView.addSubview(self.clickButton);
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
And then you can actually dequeue it in the cellForRowAtIndexPath like this.
var cell = tableView.dequeueReusableCellWithIdentifier("my-cell-identifier") as? MyCustomCellWithButton;
if (cell == nil) {
cell = MyCustomCellWithButton(style: UITableViewCellStyle.Default, reuseIdentifier: "my-cell-identifier");
}
return cell!;
Well, first of all your cellForRowAtIndexPath should probably use the dequeue mechanism so you aren't recreating cells every time when virtualizing.
But that aside, all you need to do is create the button, and add it as a sub view to the cell.
cell.addSubview(newButton)
But of course then you will have to manage the sizing and layout as appropriate.
A UITableViewCell also has a selected state and a didSelect and didDeselect method available that listens to taps on the whole cell. Perhaps that's a bit more practical since you seem to want to check/uncheck checkboxes, which is more or less the same as selecting. You could set the cell in selected state right after you dequeued it.
I am trying to create a custom cell for my UITableView but I am having some difficulty.
First off I cannot use the Interface Builder, as I am experiencing a variation on this bug in Xcode. Every time I click on an element in the Interface Builder everything in that view gets a height and width of zero and gets repositioned outside of the view. Besides, I would like to learn how to do this programmatically.
Secondly I am using the Swift language for my project. I have been trying to follow this demonstration, and doing my best to convert the Objective C code over to Swift, but whenever I run into problems I end up being stuck. I presume this is because I am not converting the code over correctly.
Thirdly I found this video but despite being fairly difficult to follow (lots of the code is just copied and pasted without much explanation to what it does or why), it still ends up using the Interface Builder to change various parts.
I have a basic UITableView set up fine. I just want to be able to add a custom cell to that table view.
Can this be done using pure programming, or do I need to use the Interface Builder?
Can anyone point me in the right direction or help me out in creating a custom cell programmatically in Swift?
Many thanks.
In general: Everything is possible in pure programming ;-)
Create a custom class for your tableView cell and there setup all the elements, properties and the visual layout. Implement the required methods init(style,reuseidentifier)
In your custom class for the UITableViewController register the custom cell class using registerClass(forCellReuseIdentifier)
Setup your delegate and datasource for the custom tableViewController
Finally, you create the cells in cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("myReuseIdentifier", forIndexPath: indexPath) as MyCustomTableViewCell
// configure the cell using its properties
return cell
}
This should be the basic steps.
If you're looking for more code, here is an example of a custom cell that I created:
// File: vDataEntryCell.swift
import UIKit
class vDataEntryCell: UITableViewCell
{
//-----------------
// MARK: PROPERTIES
//-----------------
//Locals
var textField : UITextField = UITextField()
//-----------------
// MARK: VIEW FUNCTIONS
//-----------------
///------------
//Method: Init with Style
//Purpose:
//Notes: This will NOT get called unless you call "registerClass, forCellReuseIdentifier" on your tableview
///------------
override init(style: UITableViewCellStyle, reuseIdentifier: String!)
{
//First Call Super
super.init(style: style, reuseIdentifier: reuseIdentifier)
//Initialize Text Field
self.textField = UITextField(frame: CGRect(x: 119.00, y: 9, width: 216.00, height: 31.00));
//Add TextField to SubView
self.addSubview(self.textField)
}
///------------
//Method: Init with Coder
//Purpose:
//Notes: This function is apparently required; gets called by default if you don't call "registerClass, forCellReuseIdentifier" on your tableview
///------------
required init(coder aDecoder: NSCoder)
{
//Just Call Super
super.init(coder: aDecoder)
}
}
Then in my UITableViewController class I did the following:
// File: vcESDEnterCityState.swift
import UIKit
class vcESDEnterCityState: UITableViewController
{
//-----------------
// MARK: VC FUNCTIONS
//-----------------
///------------
//Method: View Will Appear
//Purpose:
//Notes:
///------------
override func viewWillAppear(animated: Bool)
{
//First Call Super
super.viewWillAppear(animated)
//Register the Custom DataCell
tvCityStateForm.registerClass(vDataEntryCell.classForCoder(), forCellReuseIdentifier: "cell")
}
//-----------------
// MARK: UITABLEVIEW DELEGATES
//-----------------
///------------
//Method: Cell for Row at Index Path of TableView
//Purpose:
//Notes:
///------------
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
//Get Reference to Cell
var cell : vDataEntryCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as vDataEntryCell
//...Do Stuff
//Return Cell
return cell
}
}