I have two buttons within each TableView cell. When one button is tapped I want to change its appearance AND the appearance of the other button. I figured out how to change the tapped button using the approach outlined here, but am struggling with adjusting the other button.
Current relevant code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:FeedbackTableViewCell = self.feedbackTableView.dequeueReusableCell(withIdentifier: "cell") as! FeedbackTableViewCell
// Setup YES / NO Buttons
cell.feedbackYesButton.addTarget(self, action: #selector(MainSearchViewController.feedbackYesButtonTapped(sender:)), for: .touchUpInside)
cell.feedbackNoButton.addTarget(self, action: #selector(MainSearchViewController.feedbackNoButtonTapped(sender:)), for: .touchUpInside)
cell.feedbackYesButton.tag = indexPath.row
cell.feedbackNoButton.tag = indexPath.row
return cell
}
func feedbackYesButtonTapped (sender:UIButton) {
let yesButtonTag = sender.tag
switch yesButtonTag {
case 0:
// If YES button was not selected or was NO, then save value as YES and turn button "on", plus turn NO button "off".
turnFeedbackButtonOn(sender)
turnFeedbackButtonOff(NOT SURE HOW TO HANDLE THIS?)
}
// Other cases handled accordingly.
default:
return
}
}
//MARK: - Functions to change the appearances of feedback buttons
func turnFeedbackButtonOn(_ button: UIButton) {
button.setTitleColor(UIColor(red: 157/255, green: 249/255, blue: 88/255, alpha: 1 ), for: UIControlState())
button.titleLabel?.font = UIFont(name: "Avenir-Black", size: 18)
}
func turnFeedbackButtonOff(_ button: UIButton) {
button.setTitleColor(UIColor.black, for: UIControlState())
button.titleLabel?.font = UIFont(name: "Avenir", size: 17)
}
I tried passing the other button through with the target buttons, but I get an error when trying this. It feels like this should work, but I'm no expert at Swift so would appreciate any help!
cell.feedbackYesButton.addTarget(self, action: #selector(MainSearchViewController.feedbackYesButtonTapped(cell.feedbackYesButton, otherButton:cell.feedbackNoButton)), for: .touchUpInside)
func feedbackYesButtonTapped (sender:UIButton, otherButton:UIButton) {
//...
}
It would be a little easier if you handled the button events within the UITableViewCell's class instead, since you'd be able to easily reference the two buttons within there, but it's still possible to do what you want the way you're doing it:
First you'll want to get a reference to the cell after the button is pushed. It looks like you're setting the cell's row to be the button's tag, so I assume you've only got 1 section in that tableView. In that case you can get a reference to the cell by saying let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: button.tag, inSection: 0)). This return an optional, for obvious reasons, so you'll want to make sure to unwrap it safely. Then you can say turnFeedbackButtonOff(cell.feedbackNoButton) in the spot you were not sure how to handle it.
Related
I have been trying to fix an issue that I encountered with a tableviewcontroller.
The sections within the tableviewcontroller are views:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let sectionLabel = UILabel()
sectionLabel.text = Catalog.sharedInstance.allSections[section - 1].name.localized(lang: defaults.string(forKey: "language")!)
sectionLabel.font = UIFont(name: "Centabel Book", size: 25)
sectionLabel.backgroundColor = UIColor.white
sectionLabel.clipsToBounds = true
return sectionLabel
}
If I try to add a button programatically to put it over the tableview
let actionButton = JJFloatingActionButton()
override func viewDidLoad() {
super.viewDidLoad()
// Configuration of the Floating Action Button
actionButton.buttonColor = .red
actionButton.addItem { item in
self.performSegue(withIdentifier: "goToSettings", sender: nil)
}
actionButton.display(inViewController: self)
actionButton.clipsToBounds = true
// This doesn't work. It is to bring the button to the front. Now it is hidden by the sections.
view.bringSubviewToFront(actionButton)
// Checks if the language setting is nil, which means it is the first time you run the application. If then... selects English as default.
if defaults.string(forKey: "language") == nil {
defaults.set("en", forKey: "language")
}
}
... I don't know why but the viewForHeaderInSection hides the button. You can check it in the picture below:
floating button hided by the headersection
I tried to use the method:
view.bringSubviewToFront(actionButton)
and also:
actionbutton.superview?.bringSubviewToFront(actionButton)
But none of this brings the button to the front.
I am using a floating action button from github called JJFloatingActionButton. But I tried to add a simple UIButton and I got the same error. This is the code that also gave me the same error inside viewDidLoad:
let button = UIButton(frame: CGRect(x: 100, y: 1000, width: 100, height: 50))
button.backgroundColor = .green
button.setTitle("Test Button", for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
self.view.addSubview(button)
Again the same error. You can check the picture:
adding a simple UIButton happens the same problem
May be you can help me with that.
Thanks in advance.
You could try to use a regular UIViewController (not UITableViewController) and add the UITableView by hand.
Like that you would have better control over the view hierarchy.
When the app first build, it seem like working just fine, please see pic where each pics is one cell in side the table view . (I am working to place them properly)
The problem that I am facing is that: once I scroll down and scroll up again. Each cell data got massed up. Please see pics. Which I still have no idea what is wrong in my coder after hours searching online and decoding. please point out what I am missing. I have attached my code.
I am using the tutorial code from Ray Wenderlich and adding the codes below in the func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell method Yes I am doing it each word as UIBotton to trigger an action.
Adding
"cell.contentView.subviews.filter { $0 is UILabel }.forEach { $0.removeFromSuperview() }"
didn't solve the issue. please give me a completed example?(working one) Thank you.
let wholeSentense = artist.bio
let whoeSentenseArray = wholeSentense.components(separatedBy: " ")
var xPos = CGFloat(0)
for element in whoeSentenseArray {
var button = UIButton()
button.setTitle(element, for: UIControlState.normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 15)
let buttonTitleSize = (element as NSString).size(attributes: [NSFontAttributeName : UIFont.boldSystemFont(ofSize: 15 + 1)])
button.frame.size.height = buttonTitleSize.height * 1.5
button.frame.size.width = buttonTitleSize.width
button.frame.origin.x += xPos
xPos = xPos + buttonTitleSize.width + 10
button.setTitleColor(UIColor.black, for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: UIControlEvents.touchDown)
cell.addSubview(button)
}
return cell
}
func buttonAction(sender: UIButton!) {
print("\n\n Title \(String(describing: sender.titleLabel?.text)) TagNum \(sender.tag)")
// print("Button tapped")
}
I think you better move this complicated logic into a UITableViewCell subclass. Here's a rough skeleton:
class MyCustomTableViewCell : UITableViewCell {
#IBOutlet var imageView: UIImageView!
var sentence: String! {
didSet { <add your logic here> }
}
}
There are lots of resources on SO and other websites about how to create custom table view cells, such as this.
Anyway, whichever way you choose to do it, you need to be aware that table view cells are reused. You need to first remove all labels before adding them:
cell.contentView.subviews.filter { $0 is UILabel }.forEach { $0.removeFromSuperview() }
I have three buttons link, share and favourites in my cell. I have manually set the images in my custom cell class. In the willDisplay method of tableview delegate I want favourites image to update after the user taps on it. The service is working fine on the backend and the object is being added/removed from the favourites but the image is not updating. I have tried setting up the background image instead of image and also deployed various techniques prescribed for Objective-C but nothing seems to workout in my case. Initially I was setting up the image as the same in the DealsFullTableViewCell
This is willdisplay code snippet I'm talking about
if (self.dealArray[indexPath.row].imageUrl.characters.count > 0){
let cell = tableView.dequeueReusableCell(withIdentifier: "DealsFullTableViewCell", for: indexPath) as! DealsFullTableViewCell
if DataBaseReadServices().getFavourtiesforItemId(self.dealArray[indexPath.row].dealId) == nil {
cell.favouritesButton.setImage(UIImage(named: "Favourites Blue Outline 20x20"), for: .selected)
} else {
cell.favouritesButton.setImage(UIImage(named: "Favourite Icon Blue 20x20"), for: .selected)
print ("=============")
}
}
I am initializing the image in the DealsFullTableViewCell as
let favouritesButtonImage = UIImage.fontAwesomeIcon(name: .starEmpty, textColor: Constants.AppColours().primaryColour, size: CGSize(width: 20, height: 20), backgroundColor: UIColor.clear)
self.favouritesButton.setImage(favouritesButtonImage, for: .normal)
Please check with below answer, if it is not working please change the thread.
if(DataBaseReadServices().getFavourtiesforItemId(self.dealArray[indexPath.row].dealId) == nil){
cell.favouritesButton.setImage(UIImage(named: "Favourites Blue Outline 20x20"), for: .selected)
} else {
cell.favouritesButton.setImage(UIImage(named: "Favourite Icon Blue 20x20"), for: .normal)
print ("=============")
}`
Change the thread if above code is not working.
DispatchQueue.main.async {
//your code
}
Thanks
This seems to have been asked a few times in swift and objc, but I can't see a correct answer for swift so hopefully someone can help me this time. I have created a custom accessory view button, but need the correct button action: as "accessoryButtonTapped" is an unrecognised selector.
What is the selector needed to call the tableViewCellDelegate method willSelectRowAtIndexPath?
The code I have is:
let cellAudioButton = UIButton(type: .Custom)
cellAudioButton.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
cellAudioButton.addTarget(self, action: "accessoryButtonTapped", forControlEvents: .TouchUpInside) //INCORRECT ACTION:
cellAudioButton.setImage(UIImage(named: "blueSpeaker.png"), forState: .Normal)
cellAudioButton.contentMode = .ScaleAspectFit
cell.accessoryView = cellAudioButton as UIView
Thanks in advance to anyone who can help.
Why do you need to call willSelectRowAtIndexPath? You have done everything right and to solve your unrecognized selector error just make a function that will be called when you tap on the cell.accessoryView. In your case:
func accessoryButtonTapped(){
print("Tapped")
}
Update
If you want to get the indexPath you could just
Add a tag to your cellAudioButton:
cellAudioButton.tag = indexPath.row
In your addTarget add a : to pass a parameter
And in your function
func accessoryButtonTapped(sender : AnyObject){
print(sender.tag)
print("Tapped")
}
So the whole code:
let cellAudioButton = UIButton(type: .Custom)
cellAudioButton.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
cellAudioButton.addTarget(self, action: "accessoryButtonTapped:", forControlEvents: .TouchUpInside)
cellAudioButton.setImage(UIImage(named: "blueSpeaker.png"), forState: .Normal)
cellAudioButton.contentMode = .ScaleAspectFit
cellAudioButton.tag = indexPath.row
cell.accessoryView = cellAudioButton as UIView
func accessoryButtonTapped(sender : AnyObject){
print(sender.tag)
print("Tapped")
}
Swift 3.1 Updated Solution of Rashwan
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
//add button as accessory view
let cellAudioButton = UIButton(type: .custom)
cellAudioButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
cellAudioButton.addTarget(self, action: #selector(ViewController.accessoryButtonTapped(sender:)), for: .touchUpInside)
cellAudioButton.setImage(UIImage(named: "closeRed"), for: .normal)
cellAudioButton.contentMode = .scaleAspectFit
cellAudioButton.tag = indexPath.row
cell.accessoryView = cellAudioButton as UIView
return cell
}
func accessoryButtonTapped(sender : UIButton){
print(sender.tag)
print("Tapped")
}
Note:
Here ViewController is the name of Class like LoginViewController
Why don't you use the UITableViewDelegate?
#available(iOS 2.0, *)
optional func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath)
To clarify that, yet it is possible. You can have custom UI elements act as accessory button.
Here is an example – assuming you're using Interface Builder. Add a UISwitch to the custom cell. Now right-click drag-n-drop from the UITableViewCell to the UISwitch. Select accessoryView as the Outlet connection. Done.
Every time you press the switch button the delegate method will be fired. This should also work for other action elements, e.g., UIButtons. And that is exactly the use case OP asked for.
My tableview displays podcast data. All cells are reused fine and display podcasts in the right order. I add download and play buttons as subviews of each cell. When I scroll down the list, the play button which is meant to be hidden for non-downloaded episodes will be shown and will hold the data of the cell which previously initiated a download. For example if I tap download button in cell 1, it will download the right episode and the play button will play the episode perfectly. If I scroll down to cell 10, the same play button will appear and will play episode from button 1. What's wrong with my code?
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
let cell: PFTableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! PFTableViewCell
if let title = object?["title"] as? String {
cell.textLabel?.text = title
let downloadButton = UIButton(type: UIButtonType.Custom)
downloadButton.frame = CGRectMake(cell.contentView.bounds.width - 100, cell.contentView.bounds.height / 2, 100, 35)
downloadButton.setTitle("Download", forState: .Normal)
downloadButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
downloadButton.tag = indexPath.row
downloadButton.addTarget(self, action: "downloadEpisode:", forControlEvents: .TouchUpInside)
cell.addSubview(downloadButton)
let playButton = UIButton(type: UIButtonType.Custom)
playButton.frame = CGRectMake(cell.contentView.bounds.width - 100, cell.contentView.bounds.height - 89, 100, 35)
playButton.setTitle("Play", forState: .Normal)
playButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
playButton.tag = indexPath.row
playButton.addTarget(self, action: "playEpisode:", forControlEvents: .TouchUpInside)
if let isDownloaded = object?["isDownloaded"] as? String {
if isDownloaded == "yes" {
playButton.hidden = false
} else {
playButton.hidden = true
}
}
cell.addSubview(playButton)
}
return cell
}
Edit:
I've also tried this but it doesn't work and still creates 1 more button per cell:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath, object: PFObject?) -> PFTableViewCell? {
let cell: PFTableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! PFTableViewCell
if let title = object?["title"] as? String {
cell.textLabel?.text = title
}
if let button = cell.viewWithTag(indexPath.row) {
print(button)
} else {
if let isDownloaded = object?["isDownloaded"] as? String {
if isDownloaded == "yes" {
let downloadButton = UIButton(type: UIButtonType.Custom)
downloadButton.frame = CGRectMake(cell.contentView.bounds.width - 100, cell.contentView.bounds.height / 2, 100, 35)
downloadButton.setTitle("Play", forState: .Normal)
downloadButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
downloadButton.tag = indexPath.row
downloadButton.addTarget(self, action: "playEpisode:", forControlEvents: .TouchUpInside)
cell.addSubview(downloadButton)
} else {
let downloadButton = UIButton(type: UIButtonType.Custom)
downloadButton.frame = CGRectMake(cell.contentView.bounds.width - 100, cell.contentView.bounds.height / 2, 100, 35)
downloadButton.setTitle("Download", forState: .Normal)
downloadButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
downloadButton.tag = indexPath.row
downloadButton.addTarget(self, action: "downloadEpisode:", forControlEvents: .TouchUpInside)
cell.addSubview(downloadButton)
}
}
}
return cell
}
Edit2:
Added download method:
func downloadEpisode(sender: UIButton) {
print("Downloading..")
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
let object = self.objectAtIndexPath(indexPath)
if let result = object {
let urlstring = result["downloadURL"] as? String
if urlstring != nil {
let episodeURL = NSURL(string: urlstring!)
downloader.downloadPodcastEpisode(episodeURL!, podcast: result)
}
}
}
Answer:
Although all answers were right, I have decided to remove the button download/play on cell taps in the end. Many thanks!
The problem is that you're adding a new button every time. When your cell is reused, it will already have a button. You need to write code that guarantees a button will only be created once, and after that, modify the existing button instead of creating a new one.
There are two common approaches to this:
Use viewWithTag to check if a button exists (create a button and set its tag if it doesn't)
Subclass UITableViewCell, create the button in the initializer, and configure the button each time the cell is used.
Searching for "uitableviewcell viewwithtag" or "subclass uitableviewcell" should give you plenty of sample code, so I won't rehash that here.
This happens because placement of a button in a cell in your code is a one-way street: once a button is added to the cell, it is never removed or made invisible.
You need to add an else to your if statement to remove the button from the recycled cell, if that button exists.
Better yet, make the button a permanent part of the "Cell", and control its visibility in the same if statement: instead of adding the button, make it visible; instead of removing the button, make it invisible. As an added bonus, this would let you set up the visual appearance for your button in interface builder, and set some constraints, which would help avoiding "magic numbers" such as 100, 89, and 35 in the body of your code.
Looking at your code its clear you are already subclassing UITableViewCell and have custom UI, then its recommended add button in side UITableViewCell either in initialization or if you have xib for cell then add UIButton in xib (via interface builder) and create an IBOutlet for newly added button .
Now when a cell will be reused you will have that button as well, when cell is being reused its your responsibility to keep its state right (means updating its UI), for that override prepareForReuse method inside custom tableViewCell class and reset UI of cell, and in cellForRowAtIndexPath you can update UI as per new data object.