I am having some trouble with the dequeue reusable cell function in my UITableView. The tableview has a couple of cells, each of which contains a button.
When I scroll, the cell is recreated, and new buttons begin to overlap old buttons (until I have a bunch of the same buttons in the same cell). I've heard that your supposed to use the removeFromSuperview function to fix this, but i'm not really sure how to do it.
Here is a picture of my app:
And here is the cellForRowAtIndexPath (where the problem is occurring)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath)
let nameLabel: UILabel = {
let label = UILabel()
label.text = "Sample Item"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let actionButton = YSSegmentedControl(
frame: CGRect.zero,
titles: [
"No",
"Yes"
])
The reason you are seeing multiple buttons appear is because the cellForRowAtIndexPath: method is called every time a new table cell is needed. Since you are likely creating the button in that method body, it is getting recreated every time the cell is reused and you'll see them stack on top like that. The correct way to use dequeueReusableCell: with custom elements is to create a subclass of a UITableViewCell and set that as the class of your table cell in your storyboard. Then when you call dequeueReusableCell: you'll get a copy of your subclass that will contain all of your custom code. You will need to do a type conversion to get access to any of that custom code like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
if let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as? MyCustomCellClass {
cell.nameLabel.text = "Sample item"
}
// This return path should never get hit and is here only as a typecast failure fallback
return tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath);
}
Your custom cell subclass would then look something like this:
class MyCustomCellClass: UITableViewCell {
#IBOutlet var nameLabel: UILabel!
#IBOutlet var actionButton: UIButton!
#IBAction func actionButtonPressed(_ sender: UIButton) {
//Do something when this action button is pressed
}
}
You can add new label/button in cellForRowAtIndexPath, but you need to make sure there is no existing label/button before you create and add new ones. One way to do it is to set a tag to the label/button, and before you generate new label/button, check if the view with the tag is already in the cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
if let label = cell.viewWithTag(111) as? UILabel
{
label.text = "Second Labels"
}
else{
let label = UILabel()
label.tag = 111
label.text = "First Labels"
cell.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.frame = CGRect(x:0, y:10, width: 100, height:30)
}
return cell
}
Related
I am trying to show a button as an image of restaurant food and a button below that as a text label containing the name of that restaurant all in a custom table view cell. So when you scroll down the table view, you see pictures of different restaurant food and the name of the restaurant below the picture. I have a XIB file I’m using for this.
At the moment, I’m using a UIImage for the pictures of the restaurants, and this is working, but I’m trying to instead use a button with an image, so that I can make an #IBAction after the picture of the food that is a button is clicked.
I have configured a button showing restaurant names in the TableViewCell.swift file as shown below (the labels show when ran):
TableViewCell.swift code:
#IBOutlet var labelButton: UIButton!
//For retaining title in a property for IBAction use.
private var title: String = ""
func configureLabelButton(with title: String) {
self.title = title //For retaining title in a property for IBAction use.
labelButton.setTitle(title, for: .normal)
}
and implemented showing different restaurant names in each table view cell from an array with the restaurant names as strings in the view controller as shown below.
ViewController.swift code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantTableViewCell", for: indexPath) as! RestaurantTableViewCell
cell.configureLabelButton(with: myData[indexPath.row])
return cell
}
I am trying to do the same thing for showing an image in a button. I have an array of images I'm using for this same purpose called myImages. Here is the code I have so far, but it is not working when run.
TableViewCell.swift code:
#IBOutlet var imageButton: UIButton!
//For retaining image in a property for IBAction use.
private var image: UIImage = UIImage()
func configureImageButton(with image: UIImage) {
self.image = image //For retaining image in a property for IBAction use.
imageButton.setImage(image, for: .normal)
}
ViewController.swift code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantTableViewCell", for: indexPath) as! RestaurantTableViewCell
cell.configureImageButton(with: myImages[indexPath.row]!)
cell.imageButton.contentMode = .scaleAspectFill
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 185
}
I think my error is somewhere in TableViewCell.swift. I think I do not have the correct code there and potentially in ViewController.swift.
I think there is a problem in the UIImage array declaration, the issue might be it not getting an image from the array.
try with this one
var arrImg : [UIImage] = [UIImage(named: "res-1")!,UIImage(named: "res-2")!,UIImage(named: "res-3")!]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantTableViewCell", for: indexPath) as! RestaurantTableViewCell
cell.configureImageButton(with: arrImg[indexPath.row])
cell.btnResturant.contentMode = .scaleAspectFill
cell.configureLabel(with: arrRes[indexPath.row])
cell.layoutIfNeeded()
cell.selectionStyle = .none
cell.layoutIfNeeded()
return cell
}
Found the answer to my question. The syntax in TableView.swift was wrong. This may have been due to a Swift update.
Was:
imageButton.setImage(image, for: .normal)
Should be:
imageButton.setBackgroundImage(image, for: .normal)
I've made an UITableView and filled it with JSON data I get inside my API. I get and place all correctly but when I scroll or delete a row everything gets messed up!
Labels and images interfere; this is my code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var dict = productsArrayResult[indexPath.row]
let cellImage = UIImageView(frame: CGRect(x: 5, y: 5, width: view.frame.size.width / 3, height: 90))
cellImage.contentMode = .scaleAspectFit
let productMainImageString = dict["id"] as! Int
let url = "https://example.com/api/DigitalCatalog/v1/getImage?id=\(productMainImageString)&name=primary"
self.downloadImage(url, inView: cellImage)
cell.addSubview(cellImage)
let cellTitle = UILabel(frame: CGRect(x: view.frame.size.width / 3, y: 5, width: (view.frame.size.width / 3) * 1.9, height: 40))
cellTitle.textColor = UIColor.darkGray
cellTitle.textAlignment = .right
cellTitle.text = dict["title"] as? String
cellTitle.font = cellTitle.font.withSize(self.view.frame.height * self.relativeFontConstantT)
cell.addSubview(cellTitle)
let cellDescription = UILabel(frame: CGRect(x: view.frame.size.width / 3, y: 55, width: (view.frame.size.width / 3) * 1.9, height: 40))
cellDescription.textColor = UIColor.darkGray
cellDescription.textAlignment = .right
cellDescription.text = dict["description"] as? String
cellDescription.font = cellDescription.font.withSize(self.view.frame.height * self.relativeFontConstant)
cell.addSubview(cellDescription)
return cell
}
You are adding subviews multiple times while dequeuing reusable cells. What you can do is make a prototype cell either in storyboard or as xib file and then dequeue that cell at cellForRowAtIndexPath.
Your custom class for cell will look similar to this where outlets are drawn from prototype cell.
Note: You need to assign Reusable Identifier for that prototype cell.
class DemoProtoTypeCell: UITableViewCell {
#IBOutlet var titleLabel: UILabel!
#IBOutlet var descriptionLabel: UILabel!
#IBOutlet var titleImageView: UIImageView!
}
Now you can deque DemoProtoTypeCell and use accordingly.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: DemoProtoTypeCell.self), for: indexPath) as! DemoProtoTypeCell
cell.titleImageView.image = UIImage(named: "demoImage")
cell.titleLabel.text = "demoTitle"
cell.descriptionLabel.text = "Your description will go here."
return cell
}
That's because you are adding subviews to reused (so that it may already have subviews added previously) cells.
Try to check if the cell has subviews and fill in information you need, if there're no subviews then you add them to the cell.
Option 1
if let imageView = cell.viewWithTag(1) {
imageView.image = //your image
} else {
let imageView = UIImageView(//with your settings)
imageView.tag = 1
cell.addSubview(imageView)
}
Option 2
Crete UITableViewCell subclass that already has all the subviews you need.
I have used below method to remove all subviews from cell:
override func prepareForReuse() {
for views in self.subviews {
views.removeFromSuperview()
}
}
But I have created UITableViewCell subclass and declared this method in it.
you can also do one thing as #sCha has suggested. Add tags to the subviews and then use the same method to remove subview from cell:
override func prepareForReuse() {
for view in self.subviews {
if view.tag == 1 {
view.removeFromSuperview()
}
}
}
Hope this helps.
I think the other answers already mentioned a solution. You should subclass the tableview cell and just change the values of your layout elements for each row.
But I want to explain why you get this strange behaviour.
When you call
tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
it tries to reuse an already created cell with the passed identifier #"cell". This saves memory and optimises the performance. If not possible it creates a new one.
So now we got a cell with layout elements already in place and filled with your data. Your code then adds new elements on top of the old ones. Thats why your layout is messed up. And it only shows if you scroll, because the first cells got no previous cells to load.
When you subclass the cell try to create the layout only once on first initialisation. Now you can pass all values to the respective layout element and let the tableview do its thing.
Try this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell:UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil
{
cell = UITableViewCell.init(style: UITableViewCellStyle.default, reuseIdentifier: "cell")
}
for subView in cell.subviews
{
subView.removeFromSuperview()
}
// Your Code here
return cell
}
i don't know what happen, i set the button.tag with the table row and when it reach row > 1, it will throw lldb. it works if the button.tag <= 1
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cells")! as UITableViewCell
let alertBtn = cell.viewWithTag(1) as! UIButton;
alertBtn.tag = indexPath.row
alertBtn.addTarget(self, action: Selector(("showAlert:")), for: UIControlEvents.touchUpInside)
return cell
}
Application crash on this line, because it fails to find a view with tag 1, the tag is updating in every cell with row value.
let alertBtn = cell.viewWithTag(1) as! UIButton
remove this line and Take #IBOutlet for alertBtn From UITableViewCell instead of refreshing with tag.
Swift 3X...
You are replacing your tag so first tag items are getting nil so replace this code ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cells")! as UITableViewCell
let alertBtn = cell.viewWithTag(1) as! UIButton
alertBtn.addTarget(self, action: #selcetor(showAlert(sender:))), for: .touchUpInside)
return cell
}
func showAlert(sender:UIButton) {
let point = sender.convert(CGPoint.zero, to: self.tableview)
let indexpath = self.tableview.indexPathForRow(at: point)
}
Try to do custom UITableViewCell.
Declare protocol and delegate for Your new class class. Wire up a action and call delegate
protocol MyCellDelegate: class {
func buttonPressed(for cell: MyCell)
}
class MyCell:UITableViewCell {
weak var delegate: MyCellDelegate?
#IBAction func buttonPressed(sender: Any){
self.delegate?.buttonPressed(for: self)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
.......
cell.delegate = self
........
}
Remember to add new protocol implementation to Your VC. You can add prepareForReuse method and reset delegate to nil when cell is reused.
If you want to get indexPath of cell containing tapped button you can use function similar to this matching your requirement.
func showAlert(sender: AnyObject) {
if let cell = sender.superview?.superview as? UITableViewCell{ // do check your viewchierarchy in your case
let indexPath = itemTable.indexPath(for: cell)
}
print(indexPath)// you can use this indexpath to get index of tapped button
}
Remove this line from cellForRowAtIndexPath alertBtn.tag = indexPath.row
If you can use Custom Cell for this purpose you can get indexpath of selected button as you were getting previously.
Create CustomCell and create IBOutlet for your button and labels etc. You can access subviews of your cell in cellForRowAtIndexPath and assign tag to your button. If you have any queries regarding CustomCell do let me know.
This is my requirement:
I want my tableView's cell to be like the last cell, its border is margin the tableView some pix, not contradict the tableview's edge.(I want this is because when I click down the cell, there is gray effect on the cell)
How to do with that?
u can't resize the cell's, instead u can set the views's layer properties to achieve the similar effect, for example, (u are not mentioning which language u are using, i assume u are using swift).
i will assume your custom cell contains a UIView and some other view components, like below,
and also add outlet for imageHolderView in the above image,
out let name will be holderView as shown in below image,
in the custom cell class, define two methods for selection management, and your custom cell class would look like below,
class CustomCell: UITableViewCell {
#IBOutlet weak var circleNameTextField: UILabel!
#IBOutlet weak var holderView: UIView!
var cellindexPath:IndexPath?
var selectedIndexPath:IndexPath?
func selectTheCell() {
if self.selectedIndexPath?.row == self.cellindexPath?.row {
self.holderView.layer.cornerRadius = 6.0
self.holderView.layer.masksToBounds = true
self.holderView.layer.borderWidth = 4.0
self.holderView.layer.borderColor = UIColor.red.cgColor
self.backgroundColor = UIColor.gray
} else {
self.resetCellWith(animate: false)
}
}
func resetCellWith(animate:Bool) {
self.holderView.layer.cornerRadius = 0.0
self.holderView.layer.masksToBounds = false
self.holderView.layer.borderWidth = 0.0
self.holderView.layer.borderColor = UIColor.clear.cgColor
self.backgroundColor = UIColor.orange
}
}
now all u have to do is call the above methods, from controller and update the cell behaviour, for example,
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.selIndexPath = indexPath
self.aTableView.reloadSections(IndexSet(integer: 0), with: .none)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : CustomCell? = tableView.dequeueReusableCell(withIdentifier: "CUSTOM_CELL", for: indexPath) as? CustomCell//tableView.dequeueReusableCell(withIdentifier: "CUSTOM_CELL") as? CustomCell
cell?.cellindexPath = indexPath
if let selectedIndexPath = self.selIndexPath {
cell?.selectedIndexPath = selectedIndexPath
cell?.selectTheCell()
} else {
cell?.resetCellWith(animate:false)
}
cell?.selectionStyle = .none
return cell!
}
with the above arrangement, u can get the table cell and selection like below,
NOTE: well, above is one way achieve this effect. and method names i simply used the sample project that i created for different purpose. :)
I know how to pass data from a UITableViewController to another ViewController. *Both of controllers are UITableViewController.
For example:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as UITableViewCell
let viewcontroller = NextViewController()
viewcontroller.someText = "Any text"
self.navigationController?.pushViewController(viewcontroller, animated: true)
}
What this does is when I select a cell in the TableViewController, in next view controller, for example a UILabel, say myLabel, is set and a string variable is prepared as someText. And set like self.myLabel.text = someText. And when a row is selected, then in the second view controller, "Any text" will be displayed.
However, this is not what I want to use. By which I mean, my goal I am trying to achieve is:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as UITableViewCell
//In this case, the next view controller is UITableView.
let viewcontroller = NextViewController()
// I want a selected cell to lead to contain an array of String,
//so that next view controller will hold multiple cells.
// which are only unique to each cell in first TableViewController.
viewcontroller.someArray = array????
self.navigationController?.pushViewController(viewcontroller, animated: true)
}
Update
In my SecondTableViewCOntroller
There is a nvaigationBar and cells are produced by adding text by a UITextField inside UIAlertAction. These data is saved with UserDefaults.standfard with "table2". Also, there is a variable, an array of String set;
//Gloabally Declared
var myArray = [String]();
var array = [String]();
//This function is displayed in didViewLoad()
func data(){
if let myArray = savedata.stringArray(forKey: KEY) {
array = myArray
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as UITableViewCell
for object in cell.contentView.subviews
{
object.removeFromSuperview();
}
let getData = UserDefaults.standard.stringArray(forKey:"table2")
cell.textLabel?.text = getData[indexPath.row]
cell.textLabel?.font = UIFont(name: familyFont, size: 19)
cell.textLabel?.font = UIFont.systemFont(ofSize: 19)
cell.textLabel?.textColor = UIColor.darkGray
cell.accessoryType = .disclosureIndicator
cell.textLabel?.textAlignment = .center
return cell
}
In my FirstTableViewController:
In this tableViewController, cells are populated by adding text in a UITextField placed on the navigationBar. Then I want the selected cell to hold cells created in SecondTableViewController by adding text by a UITextFieldinsdie UIAlertAction. These data is saved with UserDefaults.standfard with "table1"
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as UITableViewCell
for object in cell.contentView.subviews
{
object.removeFromSuperview();
}
let getData = UserDefaults.standard.stringArray(forKey:"table1")
cell.textLabel?.myArray = getData[indexPath.row]
cell.textLabel?.font = UIFont(name: familyFont, size: 19)
cell.textLabel?.font = UIFont.systemFont(ofSize: 19)
cell.textLabel?.textColor = UIColor.darkGray
cell.accessoryType = .disclosureIndicator
cell.textLabel?.textAlignment = .center
return cell
}
I do not know how to implement code that achieves this. I have researched posts and googled so hard, but what I could find was only about passing data between a view controller and table view controller, but in my case, it is about passing data between TableView to TableView. So this might help others who have the same trouble too.
This has bugged me for a long time. Please help me...
Thanks!
You are close, and the concept stays the same. In this case you would pass the array of Strings that you want to use in the next tableView and use that array as the datasource for the second table so you would have:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = NewTableViewController()
vc.dataSourceArray = array
self.navigationController.pushViewController(vc, animated: true)
}
Then in your new tableView set your numberForSections(), numForRows(), and cellForIndexPath() based on your dataSourceArray. This will populate the new tableView with the passed data.
As per your question, I think you are trying to send your data to custom view class not to viewController class.
So I suggested to customize the init method of view class.
NB : The code I have shown below is written in Swift 2.1.
Here in the example I have taken a UIView and design table view inside that view. I passed the data from the view controller and showing the list of data as dropdown list.
Code :
In the Custom View
init(frame: CGRect, arrData: NSArray) {
super.init(frame: frame)
arrListData = NSMutableArray(array: arrData)
//In this method I have designed the table view
designListView();
}
In the ViewController :
func ListButtonClicked()
{
let arr = ["Asia", "Europe", "Africa", "North America", "South America", "Australia", "Antarctica"]
dropDownlist = DropdownView(frame: CGRectMake(0, 0, UIScreen.mainScreen().bounds.size.width, UIScreen.mainScreen().bounds.size.height), arrData: arr);
dropDownlist.backgroundColor = UIColor.clearColor();
self.view.addSubview(dropDownlist)
}
Here I have mentioned part of code, but for your convenience I am adding the output screen so that you can check whether this fulfil your requirement or not.
My output: