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.
Related
I'm trying to set an image for a button's normal state which is located in a collectionView cell. When the button is pressed the image changes. The problem is every four cells it repeats the same image as the original cell when the button is pressed. Is there a way to not have it repeat itself and when the button is pressed its only for that individual cell?
Here is the code:
class FavoritesCell: UICollectionViewCell {
var isFavorite: Bool = false
#IBOutlet weak var favoritesButton: UIButton!
#IBAction func favoritesButtonPressed(_ sender: UIButton) {
_ = self.isFavorite ? (self.isFavorite = false, self.favoritesButton.setImage(UIImage(named: "favUnselected"), for: .normal)) : (self.isFavorite = true, self.favoritesButton.setImage(UIImage(named: "favSelected"), for: .selected))
}
}
I've tried doing this but for some strange reason the 'selected' state image is never shown even when the button is pressed:
let button = UIButton()
override func awakeFromNib() {
super.awakeFromNib()
button.setImage(UIImage(named: "favUnselected"), for: .normal)
button.setImage(UIImage(named: "favSelected"), for: .selected)
}
Every time your cell is dequeued cellForItemAt is called. This is the place where you configure your cell data. So if you need to show cell marked as favourite, you can do it here.
So how do you do it there? Let's say all your cells are not selected in the beginning. Fine. You don't have to say anything in cellForItemAt. Now let's say you mark a few cells as favourite. What happens here is, it will reflect the change when the cell is visible because the button is hooked to a selector which will make the changes.
Now here is the problem. When you scroll and the cell disappears, the information about your cell being marked as favourite is lost! So what you need to do, is maintain an array which will store the IndexPath of all the selected cells. (Make sure to remove the IndexPath when a cell is removed from favourite!) Let's call that array favourites. If you can use your data source for the collection view to store the selected state information that is also fine. Now you have to store the information about whether your cell is marked as favourite in your button selector.
#objc func buttonTapped() {
if favourites.contains(indexPath) { // Assuming you store indexPath in cell or get it from superview
favourites.removeAll(where: {$0 == indexPath})
} else {
favourites.append(indexPath)
}
}
After you have stored the information about the cell, every time you dequeue a cell, you need to check if the IndexPath is favourites. If it is, you call a method which sets the cell in the selected state.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Dequeue cell and populating cell with data, etc
if favourites.contains(indexPath) {
cell.showFavourite()
}
}
Done? No! Now we have another problem. This problem is associated with the reuse of the cell. So what happens in cellForItemAt actually? You dequeue a cell and use it to display information. So when you dequeue it what happens is, it might have already been used for showing some other information in some other index path. So all the data that was existing there will persist. (Which is why you have the problem of favourites repeating every 4 cells!)
So how do we solve this? There is method in UICollectionViewCell which is called before a cell is dequeued - prepareCellForReuse. You need to implement this method in your cell and remove all the information from the cell, so that it is fresh when it arrives at cellForItemAt.
func prepareForReuse() {
//Remove label text, images, button selected state, etc
}
Or you could always set every value of everything inside the cell in cellForItemAt so that every information is always overwritten with the necessary value.
Edit: OP says he has a collection view inside a collection view. You can identify which collection view is called like this,
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView === favoriteCollectionView { // This is the collection view which contains the cell which needs to be marked as favourite
// Dequeue cell and populating cell with data, etc
if favourites.contains(indexPath) {
cell.showFavourite()
}
return cell
}
// Dequeue and return for the other collectionview
}
The cell is most likely reused and your isFavorite is set to true.
Just try adding
func prepareForReuse() {
super.prepareForReuse()
self.isFavorite = false
}
This will set the button to original image when cell is to be reused.
Also since you have your button have two states for selected why do this dance
_ = self.isFavorite ? (self.isFavorite = false, self.favoritesButton.setImage(UIImage(named: "favUnselected"), for: .normal)) : (self.isFavorite = true, self.favoritesButton.setImage(UIImage(named: "favSelected"), for: .selected))
where you could only say self.favoritesButton.selected = self.isFavorite
Change your cell code to:
class FavoritesCell: UICollectionViewCell {
#IBOutlet weak var favoritesButton: UIButton!
var isFavorite: Bool = false {
didSet {
favoritesButton.selected = isFavorite
}
}
#IBAction func favoritesButtonPressed(_ sender: UIButton) {
favoritesButton.selected = !favoritesButton.selected
}
override func prepareForReuse() {
super.prepareForReuse()
isFavorite = false
}
}
Problem I want to allow users to hit 'swap' in a table cell and then find a different Realm object to populate the 2 text labels (for exercise name and number of reps) in the cell with the values from the new object.
Research There's quite a bit (admittedly old) on 'moving rows' (e.g. here How to swap two custom cells with one another in tableview?) and also here (UITableView swap cells) and then there's obviously a lot on reloading data in itself but I can't find anything on this use case.
What have I tried my code below works fine for retrieving a new object. i.e. there's some data in the cell, then when you hit the 'swapButton' it goes grabs another one ready to put in the tableView. I know how to reload data generally but not within one particular cell in situ (the cell that the particular swap button belongs to... each cell has a 'swap button').
I'm guessing I need to somehow find the indexRow of the 'swapButton' and then access the cell properties of that particular cell but not sure where to start (I've played around with quite a few different variants but I'm just guessing so it's not working!)
class WorkoutCell : UITableViewCell {
#IBOutlet weak var exerciseName: UILabel!
#IBOutlet weak var repsNumber: UILabel!
#IBAction func swapButtonPressed(_ sender: Any) {
swapExercise()
}
func swapExercise() {
let realmExercisePool = realm.objects(ExerciseGeneratorObject.self)
func generateExercise() -> WorkoutExercise {
let index = Int(arc4random_uniform(UInt32(realmExercisePool.count)))
return realmExercisePool[index].generateExercise()
}
}
//do something here like cell.workoutName
//= swapExercise[indexRow].generateExercise().name???
}
Hold your objects somewhere in a VC that shows UITableView. Then add the VC as the target to swap button. Implement swapping objects on button press and reload data of table view after.
The whole idea is to move logic to view controller, not in separate cell.
There are 2 ways.
1. Adding VS as button action target.
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ... // get cell and configure it
cell.swapBtn.addTarget(self, action: #selector(swapTapped(_:)), for: .touchUpInside)
return cell
}
func swapTapped(_ button: UIButton) {
let buttonPosition = button.convertPoint(CGPointZero, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(buttonPosition)!
// find object at that index path
// swap it with another
self.tableView.reloadData()
}
Make VC to be delegate of cell. More code. Here you create protocol in cell and add delegate variable. Then when you create cell you assign to VC as delegate for cell:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ... // get cell and configure it
cell.delegate = self
return cell
}
func swapTappedForCell(_ cell: SwapCell) {
// the same logic for swapping
}
Solution from OP I adapted the code here How to access the content of a custom cell in swift using button tag?
Using delegates and protocols is the most sustainable way to achieve this I think.
I hope this helps others with the same problem!
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'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
}
How can I increment a value for a specific cell?
I saw this and this post. The prior I couldn't get to work (and is "brittle"?), and the former caused a segmentation fault with the functions incrementHandler() and decrementHandler().
class cell : UITableViewCell {
#IBOutlet weak var counter: UILabel
#IBAction func add (sender: AnyButton) {
counter.text = String(Int(counter.text) + 1)
// find it's corresponding stat value (from other class) and update it
stats[indexPath].counter = Int(counter.text)
}
}
class tableView: UITableViewController {
var stats = [Stat]()
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellID, forIndexPath: indexPath) as! ExpandedControllerCell
let stat = stats[indexPath.row]
cell.titleLabel.text = stat.name
cell.bigCounterLabel.text = String(stat.counter)
cell.smallCounterLabel.text = String(stat.counter)
return cell
}
}
In your cellForRowAtIndexPath, set a tag on the button. Then, in your IBAction method, use the tag to figure out which cell's button was tapped. Increment a value in an array, and then tell the cell at that indexPath to reload itself. (In cellForRowAtINdexPath, install the counter value into the cell.)
You can also write your button action so it uses the button's frame.origin to ask the table view which cell the button belongs to. That's better and less fragile.
See my answer in this thread for an explanation of how to do that.