I'm doing todolist app and I need action for button, which on tapped will do change image in a cell which I tapped on it. Cell retrieves data from Firebase to UITableView. I used button and image outlets and both reported this error:
The button from the TaskViewController to the UIButton is invalid. Outlets cannot be connected to repeating content.
I have this code for retrieves data to cell:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TasksCell", for: indexPath) as! TasksCell
let arrayTasks = tasks[indexPath.row]
cell.taskName.text = arrayTasks.content
return cell
}
and I have this in Storyboard
object left of the label is button.
The button picture is set in storyboard.
I don't know how to do it, because I don't have much experience. Please help me.
Try:
Open the Identity Inspector.
Click the prototype cell.
Set the Class to TasksCell (in my example, it is WorkTableViewCell).
Click in the Module field, empty it, and press enter. It should change to Current - project_name_here (for example, Current - Artistry.
Here's a screenshot of what it should look like. Check it to make sure you selected the right items in the right order:
Then, delete the old outlet(s) to the cell (button, label, everything) from the Connections Inspector. Do that step while selecting the cell, and then while selecting the view controller (but while selecting the view controller, only delete the outlet to the "completed" button).
And finally, re-add them by opening the code (in a new window) for your TasksCell class, not the TaskViewController class (probably in TasksCell.swift), and directly under the line:
class TasksCell: UITableViewCell {
(or similar), hold control and drag the button, label, etc. as shown:
And finally, finish creating the new outlets only from the label, button, etc. to the TasksCell class as mentioned earlier (see note below before pressing "connect"):
Change the Name as necessary. It could be something like taskName for the label, or completedButton for the button. Do not change the Type or any other fields. Press "connect", and the code from your question should work.
In your TasksCell class, connect the button to the cell, and you can access it easier (yeah agree to #ingraham, you can't connect it to your view controller):
class TasksCell: UITableViewCell {
#IBOutlet weak var buttonCheckbox: UIButton!
#IBOutlet weak var taskName: UILabel!
#IBOutlet weak var updatedDate: UILabel!
}
You can use image view for button. Also you can use this method for changing image
override func tableView(_ tableView: UITableView, didSelec tRowAt indexPath: IndexPath) {}
Edit:
I made a table view cell class:
class TableViewCell: UITableViewCell {
#IBAction func bAction(_ sender: Any) {
button.titleLabel?.text = "asdaf"
}
#IBOutlet weak var button: 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
}
}
then I edit tableView function:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell! = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) as? TableViewCell
cell.textLabel?.text = "abc"
// Configure the cell...
cell.bAction(Any.self)
return cell
}
Related
Hello the image above is the UI of my todo list app, now I just want to show the detail of item (First Item, second Item etc) when I click the detail button in the tableviewcell. So in order to get the property of the item, I need to know the indexPath of the row that I just clicked on the detail button.
I have tried some properties of the tableview like didSelectRowAt, or indexPathForSelectedRow, but both not work. For didSelectRowAt user need to click on the row first then click the detail button, and that's not what I want, and the indexPathForSelectedRow is not working for me.
A common, generalized solution for this type of problem is to connect the #IBAction of the button to a handler in the cell (not in the view controller), and then use a delegate-protocol pattern so the cell can tell the table when the button was tapped. The key is that when the cell does this, it will supply a reference to itself, which the view controller can then use to determine the appropriate indexPath (and thus the row).
For example:
Give your UITableViewCell subclass a protocol:
protocol CustomCellDelegate: class {
func cell(_ cell: CustomCell, didTap button: UIButton)
}
Hook up the #IBAction to the cell (not the view controller) and have that call the delegate method:
class CustomCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
#IBOutlet weak var customLabel: UILabel!
func configure(text: String, delegate: CustomCellDelegate) {
customLabel.text = text
self.delegate = delegate
}
#IBAction func didTapButton(_ button: UIButton) {
delegate?.cell(self, didTap: button)
}
}
Obviously, when the cell is created, call the configure method, passing, amongst other things, a reference to itself as the delegate:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let text = ...
cell.configure(text: text, delegate: self)
return cell
}
}
Finally, have the delegate method call indexPath(for:) to determine the index path for the cell in question:
extension ViewController: CustomCellDelegate {
func cell(_ cell: CustomCell, didTap button: UIButton) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
// use `indexPath.row` here
}
}
The other approach is to use closures, but again using the same general pattern of hooking the button #IBAction to the cell, but have it call a closure instead of the delegate method:
Define custom cell with closure that will be called when the button is tapped:
class CustomCell: UITableViewCell {
typealias ButtonHandler = (CustomCell) -> Void
var buttonHandler: ButtonHandler?
#IBOutlet weak var customLabel: UILabel!
func configure(text: String, buttonHandler: #escaping ButtonHandler) {
customLabel.text = text
self.buttonHandler = buttonHandler
}
#IBAction func didTapButton(_ button: UIButton) {
buttonHandler?(self)
}
}
When the table view data source creates the cell, supply a handler closure:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let text = ...
cell.configure(text: text, buttonHandler: { [weak self] cell in // the `[weak self]` is only needed if this closure references `self` somewhere
guard let indexPath = tableView.indexPath(for: cell) else { return }
// use `indexPath` here
})
return cell
}
}
I personally prefer the delegate-protocol pattern, as it tends to scale more nicely, but both approaches work.
Note, in both examples, I studiously avoided saving the indexPath in the cell, itself (or worse, “tag” values). By doing this, it protects you from getting misaligned if rows are later inserted and deleted from the table.
By the way, I used fairly generic method/closure names. In a real app, you might give them more meaningful names, e.g., didTapInfoButton, didTapSaveButton, etc.) that clarifies the functional intent.
Implement the delegate method tableView(_:accessoryButtonTappedForRowWith:)
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath)
However if you want to navigate to a different controller connect a segue to the accessory view button
If the button is a custom button see my answer in Issue Detecting Button cellForRowAt
In tableview, each cell has a bookmark image button. When tapping on it, it becomes in a selected state and the image (red bookmark icon) is shown. I have delegate method to add information in cells with selected buttons to an array:
protocol CustomPoetCellDelegate {
func cell(_ cell: CustomPoetCell, didTabFavIconFor button: UIButton)
}
class CustomPoetCell: UITableViewCell {
#IBOutlet weak var poetNameLabel: UILabel!
#IBOutlet weak var verseCountLabel: UILabel!
#IBOutlet weak var periodLabel: UILabel!
#IBOutlet weak var iconButton: UIButton!
var delegate: CustomPoetCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
iconButton.setImage(UIImage(named: "icons8-bookmark-50-red.png"), for: .selected)
iconButton.setImage(UIImage(named: "icons8-bookmark-50.png"), for: .normal)
iconButton.isSelected = false
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func favIconTapped(_ sender: UIButton) {
delegate?.cell(self, didTabFavIconFor: sender)
}
}
In tableViewController:
func cell(_ cell: CustomPoetCell, didTabFavIconFor button: UIButton) {
if !button.isSelected {
let indexPathRow = tableView.indexPath(for: cell)!.row
if isFiltering() {
favoritePoets.append(searchResults[indexPathRow])
button.isSelected = true
} else {
favoritePoets.append(poets[indexPathRow])
button.isSelected = true
}
} else {
button.isSelected = false
favoritePoets = favoritePoets.filter { $0.arabicName != cell.poetNameLabel.text}
}
}
isFiltering() method checks if searchBar is in process.
Now, if isFiltering() is false, everything is fine, but if isFiltering() is true (that is, searching for a name in tableView), when I tap on a specific cell button to select it and then click cancel to dismiss the searchBar, icons for other cells are also selected, in spite of that only the one I tapped is added to the array. When navigating back and forth from the view, wrong selections are gone and only the right one is selected.
Any idea on what's going on?
Thanks in advance.
The problem is that the UITableView re-uses cell for optimized scroll performance. Hence it re-uses the cell which is not in view anymore with a new cell about to be displayed. Therefore you need to set the state of the cell when ever it is updated/reused.
The following function is called everytime. So you need to set cell properties for selected or non-selected state over here.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.indexPath = indexPath //Create an indexPath property in your cell and set it here
//This will be required when you call the delegate method
//configure your cell state
return cell
}
You can update your models in favoritePoets array by changing the specific property in delegate method that you are already calling for e.g
favoritePoets[indexPath.row].selected = true or false
For accessing the indexPath you need to set it as mentioned in above code. And then pass it as an argument in your delegate method. Now this updated property will help you set your state in cellForRowAt function.
Hope it helps :). Feel free to comment.
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 have a button in a cell as a toggle to check in members in a club. When I check in a member, I need the button's state to stay ON after scrolling, but it turns back off. Here is the cellForRow method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.membersTableVw.dequeueReusableCell(withIdentifier: "CellMembersForCoach", for: indexPath) as! CellMembersForCoach
let member = members[indexPath.row]
cell.setMember(member)
cell.cellController = self
return cell
}
Here is the portion in the custom cell class where I toggle the button
#IBOutlet weak var checkBtn: UIButton!
#IBAction func setAttendance(_ sender: Any){
// toggle state
checkBtn.isSelected = !checkBtn.isSelected
}
The toggling works but after scrolling the table, the button state changes back to original. Any suggestion is appreciated.
This happens because you are reusing the cells.
You need to keep track of which cells have been selected. Perhaps in your member's class. Then when you are in your cellForRowAt you should check if this cell has been selected before and set the correct state for your button.
This is because of tableview is reusing your cell. so you have to maintain button as per tableView data source.
Shamas highlighted a correct way to do it, so I'll share my whole solution.
I created a singleton class to store an array of checked cells:
class Utility {
// Singleton
private static let _instance = Utility()
static var Instance: Utility{
return _instance
}
var checkedCells = [Int]()
In the custom cell class I have action method wired to the check button to add and remove checked cells:
#IBOutlet weak var checkBtn: UIButton!
#IBAction func setAttendance(_ sender: Any){
// Get cell index
let indexPath :NSIndexPath = (self.superview! as! UITableView).indexPath(for: self)! as NSIndexPath
if !checkBtn.isSelected{
Utility.Instance.checkedCells.append(indexPath.row)
}else{
// remove unchecked cell from list
if let index = Utility.Instance.checkedCells.index(of: indexPath.row){
Utility.Instance.checkedCells.remove(at: index)
}
}
// toggle state
checkBtn.isSelected = !checkBtn.isSelected
}
In the cellForRowAt method in the view controller I check if the cell row is in the array and decide if the toggle button should be checked:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.membersTableVw.dequeueReusableCell(withIdentifier: "CellMembersForCoach", for: indexPath) as! CellMembersForCoach
if Utility.Instance.checkedCells.contains(indexPath.row){
cell.checkBtn.isSelected = true
}
return cell
}
The problem is here:
checkBtn.isSelected = !checkBtn.isSelected
This code will reflect the button selection state every time the cell is configured when the delegate cellForRowAt invokes. So if you selected it before, now it turns to not-selected.
Since the tableView is reusing cells you code is not going to work.
You have to keep track of each button when selected and set it again when tableview is reusing cells when you scroll.
Solution : You can take an array(contains bool) which is size of your tableview data.
So you have to set state of button using array and update array when selected or deselected.
I have a custom UITableViewCell subclass and its associated xib. I have a UILabel and a UIButton in this cell and I have wired the touch up inside action of the button to the subclass.
What I need is when that button in the cell is tapped, to get the indexpath of the cell which has that button. And maybe send it back to the view controller via a delegate or something.
Since I'm inside a UITableViewCell subclass, I can't use a solution like this because I don't have a reference to the tableview from inside the cell subclass. Upon further investigation I found another solution and I implemented it in Swift like this.
import UIKit
class ContactCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
selectionStyle = .None
}
#IBAction func callButtonPressed(sender: UIButton) {
let indexPath = (self.superview as UITableView).indexPathForCell(self)
println("indexPath?.row")
}
}
But when I tap on the button, it crashes with an error message saying Swift dynamic cast failed.
Any idea what's wrong with my code?
Or I'm open to any other suggestions which would allow me to achieve the desired result in any other way.
Thank you.
Sounds like you need a delegate:
Delegates in swift?
Then just pass the cell itself as a parameter to the delegate, and then you can easily do tableView.indexPathForCell(cellFromDelegateMethod)
Hey you can use "Tag" of the button also.Inside the cellForRowAt method of table delegate u can tag the button with Indexpath.row . here is the example what i m tried to say.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// get ur cell nib .As it has a button
cell.startOrConntinuBtn.addTarget(self, action: #selector(sumbitOrContinue), for: .touchUpInside)
cell.startOrConntinuBtn.tag = indexPath.row }
and in the touch method "sumbitOrContinue" -
func sumbitOrContinue(sender: UIButton!) {
let tag = sender.tag
// do what you want to do like this example
let detail = self.detailList[tag]
let vc = self.storyboard?.instantiateViewController(withIdentifier: "mockExamInt") as! MockWindowVc
vc.detailId = detail.id
self.navigationController?.pushViewController(vc, animated: true)}
UIButton.Type really does not have member superview, but sender have
var cell: UITableViewCell = sender.superview.superview as UITableViewCell