iOS tableview cell button selection strange behavior - ios

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.

Related

Swift 4.2: Unable to add custom behaviour to a custom UITableViewCell using Protocol oriented delegate concepts

I've a custom UITableViewCell, in that I've two UILabels & one UIButton. I'm able to load data...and display it as per requirement.
Problem Statement-1: Now problem exist in my UIButton, which is in my UICustomTableViewCell. Due to this I'm unable to handle click event on that UIButton.
Problem Statement-2: On button Click I have to identify the index of that Button click and pass data to next ViewController using segue.
Now have a look on...what did I've tried for this...
Yes, first-of-all I have thought that Binding IBOutlet action in my CustomCell will resolve my problem...but actually it doesn't solved my problem.
After that I've accessed button using .tag and initialised index path.row to it.
But it won't helped me.
So now I'm using Protocol oriented concept using delegate to handle click event on my UIButton which is available in CustomCell.
What did I tried:
SwiftyTableViewCellDelegate:
protocol SwiftyTableViewCellDelegate : class {
func btnAuditTrailDidTapButton(_ sender: LeadCustomTableViewCell)
}
CustomTableViewCell with delegate:
class LeadCustomTableViewCell: UITableViewCell {
#IBOutlet weak var lblMeetingPersonName: UILabel!
#IBOutlet weak var lblPolicyNo: UILabel!
#IBOutlet weak var btnLeadAuditTrail: UIButton!
weak var delegate: SwiftyTableViewCellDelegate?
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
#IBAction func btnAuditTrailTapped(_ sender: UIButton) {
delegate?.btnAuditTrailDidTapButton(self)
}
}
ViewController implementing delegate:
class LeadViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, SwiftyTableViewCellDelegate {
//IBOutlet Connections - for UITableView
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
//setting dataSource & delegates of UITableView with this ViewController
self.tableView.dataSource = self
self.tableView.delegate = self
//Reloading tableview with updated data
self.tableView.reloadData()
//Removing extra empty cells from UITableView
self.tableView.tableFooterView = UIView()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:LeadCustomTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "cell") as! LeadCustomTableViewCell
//Assigning respective array to its associated label
cell.lblMeetingPersonName.text = (meetingPersonNameArray[indexPath.section] )
cell.lblPolicyNo.text = (String(policyNoArray[indexPath.section]))
cell.btnLeadAuditTrail.tag = indexPath.section
cell.delegate = self
return cell
}
//This is delegate function to handle buttonClick event
func btnAuditTrailDidTapButton(_ sender: LeadCustomTableViewCell) {
guard let tappedIndexPath = tableView.indexPath(for: sender) else { return }
print("AuditTrailButtonClick", sender, tappedIndexPath)
}
Don't know why this is not working.
Link the touch up inside event in cellForRow by adding the following code:
cell.btnLeadAuditTrail.addTarget(self, action:#selector(btnAuditTrailDidTapButton(_:)), for: .touchUpInside)

How to change image of imageview in tableview custom cell with pagination.

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

UISwitch in accessory view of a tableviewcell, passing a parameter with the selector to have selector function see the indexpath.row?

I have a UISwitch in a tableviewcontroller, and when the switch is toggled I want it to change the value of a boolean variable in an array I created inside the view controller, that the cell is related to. Kind of like the Stock Alarm App on IOS, where each cell has a UISwitch, and toggling the switch will turn off each individual alarm. So with the UISwitch, with its selector code, this is inside the cellForRowAtIndexPath method
//switch
let lightSwitch = UISwitch(frame: CGRectZero) as UISwitch
lightSwitch.on = false
lightSwitch.addTarget(self, action: #selector(switchTriggered), forControlEvents: .ValueChanged)
//lightSwitch.addTarget(self, action: "switchTriggered", forControlEvents: .ValueChanged )
cell.accessoryView = lightSwitch
I want it to do this
func switchTriggered(a: Int) {
changeValueOfArray = array[indexPath.row]
}
I don't have the code written for that part yet, but my question is, How can i let the switchTriggered function see the indexPath.row value, without passing it as an argument to the function because I can't because its a selector?
let lightSwitch = UISwitch(frame: CGRectZero) as UISwitch
lightSwitch.on = false
lightSwitch.addTarget(self, action: #selector(switchTriggered), forControlEvents: .ValueChanged)
lightSwitch.tag = indexpath.row
cell.accessoryView = lightSwitch
Let save your boolean value in Array
func switchTriggered(sender: UISwitch) {
sender.on ? array[sender.tag]=1 : array[sender.tag]=0
}
}
The basic idea is that you can capture the cell for which the switch was flipped and then use tableView.indexPath(for:) to translate that UITableViewCell reference into a NSIndexPath, and you can use its row to identify which row in your model structure needs to be updated.
The constituent elements of this consist of:
Create a model object that captures the information to be shown in the table view. For example, let's imagine that every cell contains a name of a Room and a boolean reflecting whether the light is on:
struct Room {
var name: String
var lightsOn: Bool
}
Then the table view controller would have an array of those:
var rooms: [Room]!
I'd define a UITableViewCell subclass with outlets for the label and the switch. I'd also hook up the "value changed" for the light switch to a method in that cell. I'd also set up a protocol for the cell to inform its table view controller that the light switch was flipped:
protocol RoomLightDelegate: class {
func didFlipSwitch(for cell: UITableViewCell, value: Bool)
}
class RoomCell: UITableViewCell {
weak var delegate: RoomLightDelegate?
#IBOutlet weak var roomNameLabel: UILabel!
#IBOutlet weak var lightSwitch: UISwitch!
#IBAction func didChangeValue(_ sender: UISwitch) {
delegate?.didFlipSwitch(for: self, value: sender.isOn)
}
}
I'd obviously set the base class for the cell prototype to be this UITableViewCell subclass and hook up the #IBOutlet references as well as the #IBAction for the changing of the value for the switch.
I'd then have the UITableViewDataSource methods populate the cell on the basis of the Room properties:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rooms.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SwitchCell", for: indexPath) as! RoomCell
let room = rooms[indexPath.row]
cell.roomNameLabel.text = room.name
cell.lightSwitch.setOn(room.lightsOn, animated: false)
cell.delegate = self
return cell
}
Note, the above cellForRowAtIndexPath also specifies itself as the delegate for the cell, so we'd want to implement the RoomLightDelegate protocol to update our model when the light switch is flipped:
extension ViewController: RoomLightDelegate {
func didFlipSwitch(for cell: UITableViewCell, value: Bool) {
if let indexPath = tableView.indexPath(for: cell) {
rooms[indexPath.row].lightsOn = value
}
}
}
Now, I don't want you to worry about the details of the above. Instead, try to capture some of the basic ideas:
Bottom line, to your immediate question, once you know which cell was was updated, you can inquire with the UITableView to determine what NSIndexPath that UITableViewCell reference corresponds to, using tableView.indexPath(for:).
Swift 3 Update:
let lightSwitch = UISwitch(frame: CGRect.zero) as UISwitch
lightSwitch.isOn = false
lightSwitch.addTarget(self, action: #selector(switchTriggered), for: .valueChanged)
lightSwitch.tag = indexPath.row
cell?.accessoryView = lightSwitch

Adding items to tableview with sections

I display a shopping list for different product groups as a table view with multiple sections. I want to add items with an add button for each group. So I equipped the header cell with a UIToolbar and a + symbol as a UIBarButtonItem.
Now every product group has an add button of his own:
If one add button was pressed, I have a problem identifying which one was pressed.
If I connect the add button with a seque, the function prepareForSeque(...) delivers a sender of type UIBarButtomItem, but there is no connection to the header cell from were the event was triggered.
If I connect an IBAction to the UITableViewController, the received sender is also of type UIBarButtomItem, and there is no connection to the header cell, too.
If I connect an IBAction to my CustomHeaderCell:UITableViewCell class, I am able to identify the right header cell.
This line of code returns the header cell title:
if let produktTyp = (sender.target! as! CustomHeaderCell).headerLabel.text
However, now the CustomHeaderCell class has the information I need.
But this information should be available in the UITableViewController.
I couldn't find a way to feed the information back to the UITableViewController.
import UIKit
class CustomHeaderCell: UITableViewCell {
#IBOutlet var headerLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
#IBAction func neuesProdukt(sender: UIBarButtonItem) {
if let produktTyp = (sender.target! as! CustomHeaderCell).headerLabel.text
{
print(produktTyp)
}
}
}
Here's how I typically handle this:
Use a Closure to capture the action of the item being pressed
class CustomHeaderCell: UITableViewCell {
#IBOutlet var headerLabel: UILabel!
var delegate: (() -> Void)?
#IBAction func buttonPressed(sender: AnyObject) {
delegate?()
}
}
When the Cell is created, create a closure that captures either the Index Path or the appropriate Section.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let section = indexPath.section
let cell = createCell(indexPath)
cell.delegate = { [weak self] section in
self?.presentAlertView(forSection: section)
}
}

Update the state of a UISwitch when pressed in a TableView

I have an iOS application that needs to turn on/off lights remotely. The app gets the data for the lights from parse.com and builds a tableview with every individual cell showing the name of the light and a UISwitch. I want to know how to change the boolean value stored on parse.com when I switch on or off one of the lights. The problem is that the IBAction used by the switch is not boolean and I cannot write and if statement that updates the value of the light is the switch is pressed. I have created the IBaction in my cell class and hoped that could be used by the tableviewcontroller class.
Here is part of my tableviewcontroller class
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:RelayCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as RelayCell
let label:PFObject = self.labelArray.objectAtIndex(indexPath.row) as PFObject //create the object label
cell.relayTextField.text = label.objectForKey("text") as String //put the text in the labeltextField
if (label.objectForKey("switch") as NSObject == 1) {
//cell.mySwitch = true //turn the switch on depending on the boolean value in switchColumn
cell.mySwitch.setOn(true, animated: true)
}
else{
//cell.mySwitch = false //turn the switch on depending on the boolean value in switchColumn
cell.mySwitch.setOn(false, animated: true)
}
return cell
}
This code shows the state of each independent switch however, what I want now is to be able to press on each independent button on the app and change the value on the online database.
Could you help me out since I haven't found anything online.
class RelayCell: UITableViewCell {
#IBOutlet weak var mySwitch: UISwitch!
#IBOutlet weak var relayTextField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
relayTextField.layer.borderColor = UIColor.blackColor().CGColor
relayTextField.layer.borderWidth = 0.8
relayTextField.layer.cornerRadius = 10
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func switchChangedState(sender: UISwitch) {
}
}
This is my RelayCell class that is used by the tableView method in the tableViewController class.
One way to handle this is to add a callback property to RelayCell and to call the callback from switchChangedState:
class RelayCell: UITableViewCell {
typealias SwitchCallback = (Bool) -> Void
var switchCallback: SwitchCallback?
#IBAction func switchChangedState(sender: UISwitch) {
switchCallback?(sender.on)
}
// ... rest of RelayCell
}
In your tableView:cellForRowAtIndexPath: method, set the cell's callback:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:RelayCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as RelayCell
cell.switchCallback = { [weak self] (switchIsOn) in
self?.setSwitchValue(switchIsOn, forRowAtIndexPath:indexPath)
Void()
}
// ... rest of tableView:cellForRowAtIndexPath:
return cell
}
Then you can do whatever you need in setSwitchValue:forRowAtIndexPath:, which is a method you add to your table view controller class:
private func setSwitchValue(switchIsOn: Bool, forRowAtIndexPath indexPath: NSIndexPath) {
println("row \(indexPath.row) switch on-ness is now \(switchIsOn)")
}
UISwitch has "on" property, use it to get the boolean value for current state.

Resources