I'm trying to build a custom cell with different button inside, but when I tap "follow button" in on cell and color this button it seems that other follow button get a color too...Also if I scroll up and down my tableView other button randomly get color...I'm still learning how to use custom cell...
Here there is my custom cell
class customCell: UITableViewCell
{
#IBOutlet weak var follow: UIButton!
#IBOutlet var comment: UIButton?
#IBOutlet var share: UIButton?
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cellPost = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! customCell
cellPost.tag = Array(postToPrint.keys)[indexPath.row]
cellPost.follow?.tag = Array(postToPrint.keys)[indexPath.row]
cellPost.follow?.addTarget(self, action: "follow:", forControlEvents: .TouchUpInside)
return cellaPost
}
}
Here there is my function follow
func follow(sender: UIButton)
{
let postSelected = sender.tag
// In order to get indexPath from int values
let indexPath = NSIndexPath(forRow: postSelected, inSection: 1)
let cell = table.cellForRowAtIndexPath(indexPath) as! customCell
cell.follow.setTitleColor(UIColor.redColor(), forState: UIControlState)
idPostClicked = postSelected
sendHttpRequest(postClicked : idPostClicked)
}
I'm giving as tag to every cell my dictionary keys. Later I want to take cell.tag and send it as parameter in sendHttpRequest method. If I put my button function inside my customCell I want to get the cell in which there is my followButton so I can send a request with cell.tag.
However text get colored in other cell when I click only one...:/
Hmm, I feel like you are going about this in the wrong way. I would rather move the func follow into the cell itself. There's no reason for the tableView to handle this logic since it's the cell who'd like to change elements it can access on its own. Also, if you move the logic into the cell then you won't need tags or add targets etc.
class customCell: UITableViewCell
{
#IBOutlet weak var follow: UIButton!
#IBOutlet var comment: UIButton?
#IBOutlet var share: UIButton?
// Connect the follow button to this function as an action
#IBAction followPressed(sender: UIButton) {
follow.setTitleColor(UIColor.redColor(), forState: UIControlState.Normal)
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellPost = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! customCell
return cellaPost
}
This should do the trick, I believe.
Edit
Added some missing code for UIControlState for the button. If this doesn't work could you upload a sample project? Hard to pinpoint what you are trying to do with only this piece of code.
Related
I am using a tableview in an app in which I have used pagination. The request is sent to the server and it returns items in batches of size 10. everything is working fine till now. Now I have an imageview in my tableview cells (custom). I want that when the image of that imageview toggles when user taps on it. I tried this thing in the following way:
TableviewController:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell : AdventureTableViewCell = tableView.dequeueReusableCell(withIdentifier: "adventureCell" , for: indexPath) as? AdventureTableViewCell else {
fatalError("The dequeued cell is not an instance of AdventureViewCell.")
}
cell.adventureName.text = adventureList[indexPath.row]
cell.amountLabel.text = "\(adventurePriceList[indexPath.row])$"
cell.favouriteButtonHandler = {()-> Void in
if(cell.favouriteButton.image(for: .normal) == #imageLiteral(resourceName: "UnselectedFavIcon"))
{
cell.favouriteButton.setImage(#imageLiteral(resourceName: "FavSelectedBtnTabBar"), for: .normal)
}
else
{
cell.favouriteButton.setImage(#imageLiteral(resourceName: "UnselectedFavIcon"), for: .normal)
}
}
}
CustomCell:
class AdventureTableViewCell: UITableViewCell {
#IBOutlet weak var adventureName: UILabel!
#IBOutlet weak var adventureImage: UIImageView!
#IBOutlet weak var amountLabel: UILabel!
#IBOutlet weak var favouriteButton: UIButton!
#IBOutlet weak var shareButton: UIButton!
var favouriteButtonHandler:(()-> Void)!
var shareButtonHandler:(()-> Void)!
override func awakeFromNib() {
super.awakeFromNib()
adventureName.lineBreakMode = .byWordWrapping
adventureName.numberOfLines = 0
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
override func prepareForReuse() {
adventureImage.af_cancelImageRequest()
adventureImage.layer.removeAllAnimations()
adventureImage.image = nil
}
#IBAction func favouriteButtonTapped(_ sender: Any) {
self.favouriteButtonHandler()
}
Now the problem which I am facing is that if user taps the first the imageview on any cell it changes its image, but along with that every 4th cell changes it image.
For example, if I have tapped imageview of first cell its image is changed but image of cell 5, 9, 13... also get changed.
What is wrong with my code? Did I miss anything? It is some problem with indexPath.row due to pagination, but i don't know what is it exactly and how to solve it. I found a similar question but its accepted solution didn't work for me, so any help would be appreciated.
if you need to toggle image and after scrolling also that should be in last toggle state means you need to use an array to store index position and toggle state by comparing index position and scroll state inside cellfoeRowAtIndex you can get the last toggle state that is one of the possible way to retain the last toggle index even when you scroll tableview otherwise you will lost your last toggle position
if self.toggleStatusArray[indexPath.row]["toggle"] as! String == "on"{
cell.favouriteButton.setImage(#imageLiteral(resourceName: "FavSelectedBtnTabBar"), for: .normal)
} else {
cell.favouriteButton.setImage(#imageLiteral(resourceName: "UnselectedFavIcon"), for: .normal)
}
cell.favouriteButtonHandler = {()-> Void in
if self.toggleStatusArray[indexPath.row]["toggle"] as! String == "on"{
//Assign Off status to particular index position in toggleStatusArray
} else {
//Assign on status to particular index position in toggleStatusArray
}
}
Hope this will help you
Your code looks OK, I see just one big error.
When u are setting dynamic data (names, images, stuff that changes all the time) use func tableView(UITableView, willDisplay: UITableViewCell, forRowAt: IndexPath) not cellForRowAt indexPath.
cellForRowAt indexPath should be used for static resources, and cell registration.
If u are on iOS 10 + take a look at prefetchDataSource gonna speed things up a loot, I love it.
https://developer.apple.com/documentation/uikit/uitableview/1771763-prefetchdatasource
Small example:
here u register the cell, and set up all the stuff that is common for all the cells in the table view
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "adventureCell" , for: indexPath)
cell.backgroundColor = .red
return cell
}
here adjust all the stuff that is object specific
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.nameLabel.text = model[indexPath.row].name
// also all the specific UI stuff goes here
if model[indexPath.row].age > 3 {
cell.nameLabel.textColor = .green
} else {
cell.nameLabel.textColor = .blue
}
}
You need this because cells get reused, and they have their own lifecycle, so you want to set specific data as late as possible, but you want to set the generic data as less as possible ( most of the stuff you can do once in cell init ).
Cell init is also a great place for generic data, but u can not put everything there
Also, great thing about cell willDisplay is the that u know actual size of the frame at that point
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
}
This question already has answers here:
Correct way to setting a tag to all cells in TableView
(3 answers)
Closed 6 years ago.
I have a custom UITableViewCell in which I have connected my UIButton using Interface Builder
#IBOutlet var myButton: UIButton!
Under cell configuration of UITableViewController, I have the following code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var customCell = tableView.dequeueReusableCell(withIdentifier: self.MY_CELL_IDENTIFIER, for: indexPath) as! myCustomCell
// CONFIGURE OTHER CELL PARAMETERS
customCell.myButton.tag = indexPath.row
customCell.myButton.addTarget(self, action: #selector(myButtonPressed), for: UIControlEvents.touchUpInside)
return customCell
}
Finally, I have
private func myButtonPressed(sender: UIButton!) {
let row = sender.tag
print("Button Sender row: \(row)")
}
This code is not working, unless I change the function definition to below:
#objc private func myButtonPressed(sender: UIButton!) {
let row = sender.tag
print("Button Sender row: \(row)")
}
Is there a better way to implement UIButton on custom UITableViewCell in Swift 3
I am not a big fan using view tags. Instead of this, you could use the delegate pattern for your viewController to be notified when a button has been hit.
protocol CustomCellDelegate: class {
func customCell(_ cell: UITableViewCell, didPressButton: UIButton)
}
class CustomCell: UITableViewCell {
// Create a delegate instance
weak var delegate: CustomCellDelegate?
#IBAction func handleButtonPress(sender: UIButton) {
self.delegate?.customCell(self, didPressButton: sender)
}
}
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var customCell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath) as! CustomCell
// CONFIGURE OTHER CELL PARAMETERS
//Assign the cell's delegate to yourself
customCell.delegate = self
return customCell
}
}
extension ViewController: CustomCellDelegate {
// You get notified by the cell instance and the button when it was pressed
func customCell(_ cell: UITableViewCell, didPressButton: UIButton) {
// Get the indexPath
let indexPath = tableView.indexPath(for: cell)
}
}
Yes, there is a smarter and better way to do this. The main problem of your method is that it only work if no insert, delete or move cells operation occurs. Because anyone of these operations can change de indexPath of the cells that were created for a different indexPath.
The system I use is this:
1.- Create a IBAction in your cell class MyCustomCell (With uppercase M. It is a class, so name it properly).
2.- Connect the button to that action.
3.- Declare a protocol MyCustomCellDelegate with, at least, a method
func myCustomCellButtonAction(_ cell:MyCustomCell)
and add a property to MyCustomCell
var delegate : MyCustomCellDelegate?
4.- Set the view controller as MyCustomCellDelegate
In the method of MyCustomCell connected to the button invoke the delegate method:
delegate?.myCustomCellButtonAction( self )
5.- In the view controller, in the method cellForRowAt:
customCell.delegate = self
6.- In the view controller, in the method myCustomCellButtonAction:
func myCustomCellButtonAction( _ cell: MyCustomCell ) {
let indexPath = self.tableView.indexPathForCell( cell )
// ...... continue .....
}
You can also use delegates to do the same.
Directly map the IBAction of button in your custom UITableViewCell Class.
Implement the delegate methods in viewcontroller and On button action call the delegate method from custom cell.
I have a TablewView that has prototype UITableViewCell that has its own class,
Custom UITableViewCell Class
import UIKit
class H_MissingPersonTableViewCell: UITableViewCell {
#IBOutlet weak var strName: UILabel!
#IBOutlet weak var imgPerson: UIImageView!
#IBOutlet weak var Date1: UILabel!
#IBOutlet weak var Date2: UILabel!
#IBOutlet weak var MainView: UIView!
#IBOutlet weak var btnGetUpdates: UIButton!
#IBOutlet weak var btnComment: UIButton!
#IBOutlet weak var btnReadMore: UIButton!
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
}
}
The TableView loads perfectly and the dataSource and delegate work fine. However, when I click a BUTTON at IndexPath.row = 0 (zero), and then I scroll down , I would see random(?) or other cells also being highlighted as a result of clicking BUTTON in row 0...
Here is my CellForRowAtIndexPath code:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
println("called")
var cell : CustomCell
cell = tableView.dequeueReusableCellWithIdentifier("CustomCellID", forIndexPath: indexPath) as! CustomCell
cell.strName.text = self.names[indexPath.row]
cell.imgPerson.image = UIImage(named: "\(self.persons[indexPath.row])")!
cell.btnGetUpdates.layer.borderColor = UIColor.darkGrayColor().CGColor
cell.btnGetUpdates.layer.borderWidth = 1
cell.btnGetUpdates.layer.cornerRadius = 5.0
cell.btnComment.layer.borderColor = UIColor.darkGrayColor().CGColor
cell.btnComment.layer.borderWidth = 1
cell.btnComment.layer.cornerRadius = 5.0
cell.btnReadMore.layer.borderColor = UIColor.darkGrayColor().CGColor
cell.btnReadMore.layer.borderWidth = 1
cell.btnReadMore.layer.cornerRadius = 5.0
cell.dateMissingPersonPlace.text = self.MissingPeoplePlace[indexPath.row]
cell.dateMissingPersonSince.text = self.MissingPeopleSince[indexPath.row]
cell.btnGetUpdates.tag = indexPath.row
cell.btnGetUpdates.addTarget(self, action: "GetUpdatesButton:", forControlEvents: .TouchUpInside)
return cell
}
in my cell.btnGetUpdates , I put an action as you can see in the last part of my CellForIndex code, GetUpdatesButton:
This is the code:
func GetUpdatesButton(sender: UIButton){
println("Button Clicked \(sender.tag)")
var sen: UIButton = sender
var g : NSIndexPath = NSIndexPath(forRow: sen.tag, inSection: 0)
var t : CustomCell = self.myTableView.cellForRowAtIndexPath(g) as! CustomCell
t.btnGetUpdates.backgroundColor = UIColor.redColor()
}
The problem is, NOT ONLY the button in index 0 is being highlighted, but I would also see random buttons from other index/rows being highlighted as I scroll my tableView down.
Where did I go wrong, how can I only update the button I clicked...and not other buttons..
I have 20 items ( 20 rows) and when I clicked button in row 0, rows 3, 6, 9....are also having highlighted buttons.
ROW 0 , button clicked
ROW 3, button clicked as well, though I did not really click it.
This is occurring due to UITableview have reusablecell policy.In order to resolve this issue You need to maintain one array of selected items in cellForRowAtIndexPath method you have to verify weather this button is being hold by selected item array. if yes then apply selection styles otherwise apply normal style to it.
Check below source code for buttonclick:
func GetUpdatesButton(sender: UIButton)
{
var sen: UIButton = sender
var g : NSIndexPath = NSIndexPath(forRow: sen.tag, inSection: 0)
var t : CustomCell = self.myTableView.cellForRowAtIndexPath(g) as! CustomCell
t.btnGetUpdates.backgroundColor = UIColor.redColor()
self.selectedButtonsArray.addObject(indexpath.row)
}
Below code for applying styles on buttons in CellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell : CustomCell
cell = tableView.dequeueReusableCellWithIdentifier("CustomCellID", forIndexPath: indexPath) as! CustomCell
cell.strName.text = self.names[indexPath.row]
cell.imgPerson.image = UIImage(named: "\(self.persons[indexPath.row])")!
if(self.selectedButtonsArray.containsObject(indexpath.row)){
cell.btnGetUpdates.backgroundColor = UIColor.redColor()
cell.btnGetUpdates.layer.borderColor = UIColor.darkGrayColor().CGColor
cell.btnGetUpdates.layer.borderWidth = 1
cell.btnGetUpdates.layer.cornerRadius = 5.0
}else{
cell.btnGetUpdates.layer.borderColor = UIColor.darkGrayColor().CGColor
cell.btnGetUpdates.layer.borderWidth = 1
cell.btnGetUpdates.layer.cornerRadius = 5.0
}
cell.btnComment.layer.borderColor = UIColor.darkGrayColor().CGColor
cell.btnComment.layer.borderWidth = 1
cell.btnComment.layer.cornerRadius = 5.0
cell.btnReadMore.layer.borderColor = UIColor.darkGrayColor().CGColor
cell.btnReadMore.layer.borderWidth = 1
cell.btnReadMore.layer.cornerRadius = 5.0
cell.dateMissingPersonPlace.text = self.MissingPeoplePlace[indexPath.row]
cell.dateMissingPersonSince.text = self.MissingPeopleSince[indexPath.row]
cell.btnGetUpdates.tag = indexPath.row
cell.btnGetUpdates.addTarget(self, action: "GetUpdatesButton:",forControlEvents: .TouchUpInside)
return cell
}
I hope this helps to resolve your problem! Thanks.
Inside the GetUpdatesButton() function , keep a flag value for the indexPath.row value in a separate array. Then reload the specific cell of the table view.
Now in the cellForRowAtIndexPath function make a condition of checking the flag value is set or not, If set then change background colour else set the default colour.
PS. there are different kinds of work arounds to solve the issue. Just understand the logic and go for the one which is useful in the code.
This is because cells are being reused.
So when you colour the cells using the button action, the cell's background is being painted which i guess is working fine but the situation is that when you scroll up or down, the previous cell which is having bg colour is being reused and hence you get the previous settings it was.
A tip would be to explicitly set the background colour of the UIButton and UITableViewCell in this method func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell, as everytime it will be reused the colour will be set as default and you will NOT get the previous colour it was being reused from.
Just set
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
cell.backgroundColor = [UIColor whiteColor];
cell.btnGetUpdates setBackgroundColor:[UIColor whiteColor];
//with along the other code, add these two lines.
}
EDIT
for getting an effect like you want set an instance variable which stores the indexPath of the selected Cell something like an int.
In the button action method store the int to the button.tag property which is also the indexpath.row property of the cell.
in your cellForRowAtIndexPath method, do a check
if(indexpath.row == selectedIntIndexPath){
//change the colour whatever you want.
}
Finally in the IBAction Method of the button call [self.tableView reloadData]
So the complete code will be something like this
#implementation ViewController{
int selectedIntIndexPath;
}
-(IBAction)actionForButton:(id)sender{
UIButton *btn = (UIButton *)sender;
selectedIntIndexPath = btn.tag;
[self.customTableView reloadData];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UICustomTableViewCell *cell = (UICustomTableViewCell *)[tableView dequeueReusableCellWithIdentifier:#"reuseIdentifire" forIndexPath:indexPath];
if(indexPath.row == selectedIntIndexPath){
// do your colour
}
return cell;
}
Here my solutions, in order to expand #RaymondLedCarasco comment:
Tested in Swift 4 Xcode 9.3
Declare a propierty:
var numberCells: [Int] = []
When click the button of Cell:
self.numberCells.append(indexPath.row)
In CellForRowAtIndex check:
if self.numberCells.contains(indexPath.row) { ... } else { ... }
Here most of TableViewCell problems I have solved, Like when clicking the cell button after scrolling the remaining rows may be changed and when selecting at didSelectRowAt indexPath: IndexPath the row, after scrolling the remaining rows may be selected. So I created an app to solve those problems. Here adding GitHub code Find the code
In the first screen, click on a red button and just scroll down you can't see other cells are affected automatically in UITableViewCell.
Clicking on didSelectRowAt indexPath: IndexPath in the first screen you can go to details screen that's the second screen, in that screen after selecting a row other rows won't be selected automatically in UITableViewCell.
I have this:
let mapsbut = cell.viewWithTag(912) as! UIButton
mapsbut.addTarget(self, action: "mapsHit:", forControlEvents: UIControlEvents.TouchUpInside)
and
func mapsHit(){
// get indexPath.row from cell
// do something with it
}
How is this accomplished?
You can always use the tag from your button to pass or hold a value, or a var inside of your custom cell implementation. For example, if you have your button as an outlet in your UITableViewCell (for instance):
class MenuViewCell: UITableViewCell {
#IBOutlet weak var titlelabel: UILabel!
#IBOutlet weak var button: UIButton! {
didSet {
button.addTarget(self, action: "mapsHit:", forControlEvents: UIControlEvents.TouchUpInside)
}
}
func mapsHit(sender: UIButton){
let indexPathOfThisCell = sender.tag
println("This button is at \(indexPathOfThisCell) row")
// get indexPath.row from cell
// do something with it
}
}
Notice here, that you need to set sender:UIButton as a parameter when you set "mapsHit:". This will be the button itself in wich the user has tapped.
Now, for this to work, your tag can not be "912". Instead when you build your cell, assign to the tag property of your button, the value of it's indexPath.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MenuViewCell", forIndexPath: indexPath) as! MenuViewCell
cell.titlelabel?.text = data[indexPath.row].description
cell.button.tag = indexPath.row
return cell
}
...one solution could be to have a var inside your class holding the indexPath of the last cell tapped, then you can use that value inside your mapsHit() function.