UIButton inside custom UITableViewCell causes button to be selected in multiple cells - ios

The goal of the UITableView is to display names with a custom styled checkbox beside it. But somehow when I tap the checkbox, multiple checkboxes in the UITableView get selected.
I've googled and implemented multiple solutions from different questions but none of them seem to work.
Custom UITableViewCell
import UIKit
class NameTableViewCell: UITableViewCell {
var name = UILabel()
let checkboxImage_unchecked = UIImage(named: "cellViewCheckbox_unchecked")
let checkboxImage_checked = UIImage(named: "cellViewCheckbox_checked")
let checkbox:UIButton
weak var delegate:HandleCellInteractionDelegate?
override init(style: UITableViewCellStyle, reuseIdentifier: String!) {
self.checkbox = UIButton(frame: CGRectMake(35, 16, self.checkboxImage_unchecked!.size.width/2, self.checkboxImage_unchecked!.size.height/2))
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = UIColor.transparant()
self.checkbox.setBackgroundImage(self.checkboxImage_unchecked, forState: .Normal)
self.checkbox.setBackgroundImage(self.checkboxImage_checked, forState: .Selected)
self.name = UILabel(frame: CGRectMake(97,18,200, 30))
self.addSubview(self.name)
self.addSubview(self.checkbox)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func checkboxPushed(sender:AnyObject){
self.checkbox.selected = !self.checkbox.selected
self.delegate?.didPressButton(self)
}
In the protocol I declare a method didPressButton as a callback for when the button is pressed (the delegate needs to implement it)
Protocol
import UIKit
protocol HandleCellInteractionDelegate:class {
func didPressButton(cell:NameTableViewCell)
}
The ViewController
import UIKit
class NameViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, HandleCellInteractionDelegate{
var nameView:NameView {
get {
return self.view as! NameView
}
}
var names:[Name]?
var firstLetters:[String]?
let cellIdentifier = "nameCell"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//self.view.backgroundColor = UIColor.darkSalmon()
}
override func loadView() {
let bounds = UIScreen.mainScreen().bounds
self.view = NameView(frame: bounds)
if(NSUserDefaults.standardUserDefaults().objectForKey("gender") !== nil){
self.loadJSON()
self.getFirstLetters();
self.nameView.tableView.delegate = self;
self.nameView.tableView.dataSource = self;
self.nameView.tableView.registerClass(NameTableViewCell.classForCoder(), forCellReuseIdentifier: "nameCell")
self.nameView.tableView.separatorStyle = .None
self.nameView.tableView.rowHeight = 66.5 }
}
func loadJSON(){
let path = NSBundle.mainBundle().URLForResource("names", withExtension: "json")
let jsonData = NSData(contentsOfURL: path!)
self.names = NamesFactory.createFromJSONData(jsonData!, gender: NSUserDefaults.standardUserDefaults().integerForKey("gender"))
}
func getFirstLetters(){
var previous:String = ""
for name in self.names! {
let firstLetter = String(name.name.characters.prefix(1))
if firstLetter != previous {
previous = firstLetter
self.firstLetters?.append(firstLetter)
print(firstLetter)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (self.names!.count)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return nameCellAtIndexPath(indexPath)
}
func nameCellAtIndexPath(indexPath:NSIndexPath) -> NameTableViewCell {
let cell = self.nameView.tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! NameTableViewCell
self.setNameForCell(cell, indexPath: indexPath)
cell.delegate = self;
cell.checkbox.addTarget(cell, action: "checkboxPushed:", forControlEvents: .TouchUpInside)
return cell
}
func setNameForCell(cell:NameTableViewCell, indexPath:NSIndexPath) {
let item = self.names![indexPath.row] as Name
cell.name.text = item.name
}
func didPressButton(cell: NameTableViewCell) {
print(cell)
}
}
What am I doing wrong & how can I fix it?

You must have to save the check box state. Add one more attribute say checkboxStatus to your Model(Name).I hope you will have indexpath.row.
After this line in nameCellAtIndexPath
cell.delegate = self;
set the tag like this
cell.tag = indexPath.row;
In the delegate method,
func didPressButton(cell: NameTableViewCell) {
print(cell)
let item = self.names![cell.tag] as Name
item.checkboxstatus = cell.checkbox.selected
}
Just one crucial change,
func nameCellAtIndexPath(indexPath:NSIndexPath) -> NameTableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! NameTableViewCell
self.setNameForCell(cell, indexPath: indexPath)
cell.delegate = self;
cell.tag = indexPath.row;
cell.checkbox.addTarget(cell, action: "checkboxPushed:", forControlEvents: .TouchUpInside)
let item = self.names![cell.tag] as Name
cell.checkbox.selected = item.checkboxstatus
return cell
}
Cheers!

This happens because the UITableViewCell are being reused.
You changed the cell when you press the checkbox, you need to keep track of that in your data source model.
In cellForRowAtIndexPath you have to add a condition to check if that checkbox was checked or not. Then you display the appropriate view accordingly.
Edit:
In your example you need to check in nameCellAtIndexPath if the checkbox is checked or not and then display it correct.

Related

UITableView CustomCell Reuse (ImageView in CustomCell)

I'm pretty new to iOS dev and I have an issue with UITableViewCell.
I guess it is related to dequeuing reusable cell.
I added an UIImageView to my custom table view cell and also added a tap gesture to make like/unlike function (image changes from an empty heart(unlike) to a filled heart(like) as tapped and reverse). The problem is when I scroll down, some of the cells are automatically tapped. I found out why this is happening, but still don't know how to fix it appropriately.
Below are my codes,
ViewController
import UIKit
struct CellData {
var title: String
var done: Bool
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var models = [CellData]()
private let tableView: UITableView = {
let table = UITableView()
table.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.identifier)
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
configure()
}
private func configure() {
self.models = Array(0...50).compactMap({
CellData(title: "\($0)", done: false)
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = models[indexPath.row]
guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.textLabel?.text = model.title
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
tableView.reloadData()
}
}
TableViewCell
import UIKit
class TableViewCell: UITableViewCell {
let mainVC = ViewController()
static let identifier = "TableViewCell"
let likeImage: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "heart")
imageView.tintColor = .darkGray
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(likeImage)
layout()
//Tap Gesture Recognizer 실행하기
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
likeImage.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
}
private func layout() {
likeImage.widthAnchor.constraint(equalToConstant: 30).isActive = true
likeImage.heightAnchor.constraint(equalToConstant: 30).isActive = true
likeImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
likeImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
}
#objc func didTapImageView(_ sender: UITapGestureRecognizer) {
if likeImage.image == UIImage(systemName: "heart.fill"){
likeImage.image = UIImage(systemName: "heart")
likeImage.tintColor = .darkGray
} else {
likeImage.image = UIImage(systemName: "heart.fill")
likeImage.tintColor = .systemRed
}
}
}
This gif shows how it works now.
enter image description here
I've tried to use "done" property in CellData structure to capture the status of the uiimageview but failed (didn't know how to use that in the correct way).
I would be so happy if anyone can help this!
You've already figured out that the problem is cell reuse.
When you dequeue a cell to be shown, you are already setting the cell label's text based on your data:
cell.textLabel?.text = model.title
you also need to tell the cell whether to show the empty or filled heart image.
And, when the user taps that image, your cell needs to tell the controller to update the .done property of your data model.
That can be done with either a protocol/delegate pattern or, more commonly (particularly with Swift), using a closure.
Here's a quick modification of the code you posted... the comments should give you a good idea of what's going on:
struct CellData {
var title: String
var done: Bool
}
class ShinViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var models = [CellData]()
private let tableView: UITableView = {
let table = UITableView()
table.register(ShinTableViewCell.self, forCellReuseIdentifier: ShinTableViewCell.identifier)
return table
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.delegate = self
tableView.dataSource = self
configure()
}
private func configure() {
self.models = Array(0...50).compactMap({
CellData(title: "\($0)", done: false)
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ShinTableViewCell.identifier, for: indexPath) as! ShinTableViewCell
let model = models[indexPath.row]
cell.myLabel.text = model.title
// set the "heart" to true/false
cell.isLiked = model.done
// closure
cell.callback = { [weak self] theCell, isLiked in
guard let self = self,
let pth = self.tableView.indexPath(for: theCell)
else { return }
// update our data
self.models[pth.row].done = isLiked
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
class ShinTableViewCell: UITableViewCell {
// we'll use this closure to communicate with the controller
var callback: ((UITableViewCell, Bool) -> ())?
static let identifier = "TableViewCell"
let likeImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(systemName: "heart")
imageView.tintColor = .darkGray
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
let myLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
// we'll load the heart images once in init
// instead of loading them every time they change
var likeIMG: UIImage!
var unlikeIMG: UIImage!
var isLiked: Bool = false {
didSet {
// update the image in the image view
likeImageView.image = isLiked ? likeIMG : unlikeIMG
// update the tint
likeImageView.tintColor = isLiked ? .systemRed : .darkGray
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// make sure we load the heart images
guard let img1 = UIImage(systemName: "heart"),
let img2 = UIImage(systemName: "heart.fill")
else {
fatalError("Could not load the heart images!!!")
}
unlikeIMG = img1
likeIMG = img2
// add label and image view
contentView.addSubview(myLabel)
contentView.addSubview(likeImageView)
layout()
//Tap Gesture Recognizer 실행하기
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
likeImageView.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
}
override func prepareForReuse() {
super.prepareForReuse()
}
private func layout() {
// let's use the "built-in" margins guide
let g = contentView.layoutMarginsGuide
// image view bottom constraint
let bottomConstraint = likeImageView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
// this will avoid auto-layout complaints
bottomConstraint.priority = .required - 1
NSLayoutConstraint.activate([
// constrain label leading
myLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
// center the label vertically
myLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// constrain image view trailing
likeImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
// constrain image view to 30 x 30
likeImageView.widthAnchor.constraint(equalToConstant: 30),
likeImageView.heightAnchor.constraint(equalTo: likeImageView.widthAnchor),
// constrain image view top
likeImageView.topAnchor.constraint(equalTo: g.topAnchor),
// activate image view bottom constraint
bottomConstraint,
])
}
#objc func didTapImageView(_ sender: UITapGestureRecognizer) {
// toggle isLiked (true/false)
isLiked.toggle()
// inform the controller, so it can update the data
callback?(self, isLiked)
}
}

TableView inside a custom UITableViewCell not appearing for all of the cells of that custom cellviewtype

I am trying to create a table of services such that, if service has a couple of sub-services, then the cell associated with that service then has another table view showing those sub-services under the said service.
I tried implementing such a table by looking at the example shown in the link: table within a tableviewcell
I am posting related source codes associated with the tableview
BookingServiceChargeViewCell.swift
import UIKit
import PineKit
import SwiftMoment
class BookingServiceChargeViewCell: UITableViewCell, UITableViewDelegate, UITableViewDataSource {
var service : Service? = nil
var subServices : [Service] = []
let content = PineCardView()
var cover = UIImageView()
let serviceName = PineLabel.Bold(text: " ... ")
var itemIndex = -1
var chosen = false
var parentView : OnboardingChosenServicesViewController? = nil
var anchor = UIView()
let table = UITableView()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
layout()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layout() {
self.addSubview(content)
content.snp.makeConstraints { (make) in
make.top.left.right.equalTo(self).inset(5)
make.bottom.equalTo(self)
}
layoutContent()
}
func layoutContent() {
content.addSubviews([cover, serviceName])
self.cover.image = UIImage(named: "gray-card")
cover.clipsToBounds = true
cover.snp.makeConstraints { (make) in
make.width.equalTo(content).multipliedBy(0.15)
make.left.equalTo(content).offset(10)
make.top.equalTo(content.snp.top).offset(15)
make.size.equalTo(50)
}
serviceName.textColor = UIColor.black
serviceName.font = Config.Font.get(.Bold, size: 17.5)
serviceName.snp.makeConstraints { (make) in
make.centerY.equalTo(cover)
make.left.equalTo(cover.snp.right).offset(20)
}
table.delegate = self
table.dataSource = self
table.register(BookingSubServicesChargeViewCell.self, forCellReuseIdentifier: "cell")
table.separatorStyle = .none
self.content.addSubview(table)
table.snp.makeConstraints { (make) in
make.top.equalTo(self.cover.snp.bottom).offset(15)
make.left.equalTo(self.cover.snp.right).offset(10)
make.right.equalTo(self.content.snp.right).offset(-10)
make.height.equalTo(450)
}
}
func configure(_ service: Service, subServices: [Service], index: Int, parentView: OnboardingChosenServicesViewController) {
self.service = service
self.subServices = subServices
self.itemIndex = index
self.parentView = parentView
if (self.service!.defaultImage != nil){
ImageLoader.sharedLoader.imageForUrl(urlString: self.service!.defaultImage!) { (image, url) in
self.cover.image = image
}
}
self.serviceName.text = self.service!.name!
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.subServices.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! BookingSubServicesChargeViewCell
cell.configure(self.subServices[indexPath.row], index: indexPath.row, parentView: self.parentView!)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
}
BookingSubServicesChargeViewCell.swift
import UIKit
import PineKit
import SwiftMoment
class BookingSubServicesChargeViewCell: UITableViewCell {
var service : Service? = nil
let content = PineCardView()
var cover = UIImageView()
let serviceName = PineLabel.Bold(text: " ... ")
var itemIndex = -1
var chosen = false
var parentView : OnboardingChosenServicesViewController? = nil
var anchor = UIView()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.selectionStyle = .none
layout()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func layout() {
self.addSubview(content)
content.snp.makeConstraints { (make) in
make.top.left.right.equalTo(self).inset(5)
make.bottom.equalTo(self)
}
layoutContent()
}
func layoutContent() {
content.addSubviews([cover, serviceName])
self.cover.image = UIImage(named: "gray-card")
cover.clipsToBounds = true
cover.snp.makeConstraints { (make) in
make.width.equalTo(content).multipliedBy(0.15)
make.left.equalTo(content).offset(10)
make.top.equalTo(content.snp.top).offset(15)
make.size.equalTo(50)
}
serviceName.textColor = UIColor.black
serviceName.font = Config.Font.get(.Bold, size: 17.5)
serviceName.snp.makeConstraints { (make) in
make.centerY.equalTo(cover)
make.left.equalTo(cover.snp.right).offset(20)
}
self.anchor = self.serviceName
}
func configure(_ service: Service, index: Int, parentView: OnboardingChosenServicesViewController) {
self.service = service
self.itemIndex = index
self.parentView = parentView
if (self.service!.defaultImage != nil){
ImageLoader.sharedLoader.imageForUrl(urlString: self.service!.defaultImage!) { (image, url) in
self.cover.image = image
}
}
self.serviceName.text = self.service!.name!
}
}
Here a couple of screenshots of the situation that has arisen:-
As you can see in the screenshots, some of the tables which have sub-services are not being shown, but sometimes they are being shown.
Can someone tell me what is it that I am missing here? What changes am I supposed to make in the source codes shown above?
FYI: I am not using storyboards in any way and I do not know what nib files are. I have constructed this programmatically and I am hoping that I can get a code snippet based solution as soon as possible.
Thanks.
In BookingServiceChargeViewCell class, call self.table.reloadData() at the end of configure method as below.
func configure(_ service: Service, subServices: [Service], index: Int, parentView: OnboardingChosenServicesViewController) {
self.service = service
self.subServices = subServices
self.itemIndex = index
self.parentView = parentView
if (self.service!.defaultImage != nil){
ImageLoader.sharedLoader.imageForUrl(urlString: self.service!.defaultImage!) { (image, url) in
self.cover.image = image
}
}
self.serviceName.text = self.service!.name!
self.table.reloadData()
}

Both custom cell and default cell is showing together on tableview

I am making a simple tableview with a customCell. and a searchBar above. but getting a strange behavior from tableView. customCell is showing but above it defaultCell is showing as well and data is getting populated into the defaultCell though i am setting data on my customCell.
this is the output i am getting
https://i.imgur.com/xsTIsiz.png
if you look at it closely you will see my custom cell UI is showing under the default cell.
My custom cell:
https://i.imgur.com/J4UpRle.png
This is my code from viewcontroller
import UIKit
class SuraSearchController: UIViewController {
let searchController = UISearchController(searchResultsController: nil)
let reciters = [Reciter(name: "Abdul Basit Abdus Samad", downloadUrl: ""),
Reciter(name: "Abdul Rahman Al-Sudais", downloadUrl: ""),
Reciter(name: "Ali Bin Abdur Rahman Al Huthaify", downloadUrl: ""),
Reciter(name: "Mishary Rashid Alafasy", downloadUrl: ""),
Reciter(name: "Cheik Mohamed Jibril", downloadUrl: ""),
Reciter(name: "Mohamed Siddiq El-Minshawi", downloadUrl: ""),
Reciter(name: "Mahmoud Khalil Al-Hussary", downloadUrl: ""),
Reciter(name: "Ibrahim Walk (English Only)", downloadUrl: ""),
Reciter(name: "Abu Bakr Al Shatri", downloadUrl: "")]
var filteredReciters = [Reciter]()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// definesPresentationContext = true
initNavBar()
initTableView()
// Do any additional setup after loading the view.
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
func initNavBar() {
// show navbar
self.navigationController?.setNavigationBarHidden(false, animated: true)
// set search bar delegates
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
// Customize Search Bar
searchController.searchBar.placeholder = "Search Friends"
let myString = "Cancel"
let myAttribute = [ NSAttributedStringKey.foregroundColor: UIColor.white ]
UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).title = myString
UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).setTitleTextAttributes(myAttribute, for: .normal)
if #available(iOS 11.0, *) {
let scb = searchController.searchBar
scb.tintColor = UIColor.white
scb.barTintColor = UIColor.white
if let textfield = scb.value(forKey: "searchField") as? UITextField {
textfield.textColor = UIColor.blue
if let backgroundview = textfield.subviews.first {
// Background color
backgroundview.backgroundColor = UIColor.white
// Rounded corner
backgroundview.layer.cornerRadius = 10
backgroundview.clipsToBounds = true
}
}
}
// Set search bar on navbar
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
}
func initTableView() {
tableView.dataSource = self
tableView.delegate = self
tableView.register(UINib(nibName: "SuraSearchCell", bundle: nil), forCellReuseIdentifier: "SuraSearchCell")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredReciters = reciters.filter({( reciter : Reciter) -> Bool in
return reciter.name.lowercased().contains(searchText.lowercased())
})
tableView.reloadData()
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
extension SuraSearchController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
return filteredReciters.count
}
return reciters.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "SuraSearchCell", for: indexPath) as? SuraSearchCell {
let candy: Reciter
if isFiltering() {
candy = filteredReciters[indexPath.row]
} else {
candy = reciters[indexPath.row]
}
cell.textLabel!.text = candy.name
return cell
}
return UITableViewCell()
}
}
extension SuraSearchController: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
And code of the tableview cell
import UIKit
class SuraSearchCell: UITableViewCell {
#IBOutlet weak var itemTitle: UILabel!
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
}
func configureCell(item: Reciter) {
itemTitle.text = item.name
}
}
cell.textLabel is default UITableViewCell property. you just need to set value to your customCell itemTitle label.
Replace cell.textLabel!.text = candy.name with cell. itemTitle!.text = candy.name like below.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "SuraSearchCell", for: indexPath) as? SuraSearchCell {
let candy: Reciter
if isFiltering() {
candy = filteredReciters[indexPath.row]
} else {
candy = reciters[indexPath.row]
}
cell. itemTitle!.text = candy.name
//OR
cell.configureCell(candy) // IF your candy is of Reciter type
return cell
}
return UITableViewCell()
}
There may be a mistake with reuse identifier name. Match the name with the used one in the code.
Let update cellForRowAtIndex as below
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "SuraSearchCell", for: indexPath) as? SuraSearchCell {
let candy: Reciter
if isFiltering() {
candy = filteredReciters[indexPath.row]
} else {
candy = reciters[indexPath.row]
}
cell.configureCell(candy)
return cell
}
return UITableViewCell()
}

How to preserve user input in UITableViewCell before dequeue

I'm creating an application in which I need the users to fill out a number of inputs in a UITableViewCell, kinda like a form. When the user taps on the done button, I need to collect those inputs so I can run some calculations and output them on another view controller
Here is the method I used to collect those inputs:
func doneButtonTapped() {
var dict = [String: Any]()
for rows in 0...TableViewCells.getTableViewCell(ceilingType: node.ceilingSelected, moduleType: node.moduleSelected).count {
let ip = IndexPath(row: rows, section: 0)
let cells = tableView.cellForRow(at: ip)
if let numericCell = cells as? NumericInputTableViewCell {
if let text = numericCell.userInputTextField.text {
dict[numericCell.numericTitleLabel.text!] = text
}
} else if let booleanCell = cells as? BooleanInputTableViewCell {
let booleanSelection = booleanCell.booleanToggleSwitch.isOn
dict[booleanCell.booleanTitleLabel.text!] = booleanSelection
}
}
let calculator = Calculator(userInputDictionary: dict, ceiling_type: node.ceilingSelected)
}
The problem I'm having is when the cell is out of view, the user's input is cleared from the memory. Here are two screenshots to illustrate my point:
As you can see, when all the cells appears, the done button managed to store all the inputs from the user, evidently from the console print. However, if the cells are out of view, the inputs from area/m2 are set to nil:
The solution that came to mind was I shouldn't use a dequeue-able cell as I do want the cell to be in memory when it is out-of-view, but many of the stackover community strong against this practice. How should I solve this problem? Thanks!
UPDATE
Code for cellForRow(at: IndexPath)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let node = node else {
return UITableViewCell()
}
let cellArray = TableViewCells.getTableViewCell(ceilingType: node.ceilingSelected, moduleType: node.moduleSelected)
switch cellArray[indexPath.row].cellType {
case .numericInput :
let cell = tableView.dequeueReusableCell(withIdentifier: "numericCell", for: indexPath) as! NumericInputTableViewCell
cell.numericTitleLabel.text = cellArray[indexPath.row].title
return cell
case .booleanInput :
let cell = tableView.dequeueReusableCell(withIdentifier: "booleanCell", for: indexPath) as! BooleanInputTableViewCell
cell.booleanTitleLabel.text = cellArray[indexPath.row].title
return cell
}
}
}
My two custom cells
NumericInputTableViewCell
class NumericInputTableViewCell: UITableViewCell {
#IBOutlet weak var numericTitleLabel: UILabel!
#IBOutlet weak var userInputTextField: UITextField!
}
BooleanInputTableViewCell
class BooleanInputTableViewCell: UITableViewCell {
#IBOutlet weak var booleanTitleLabel: UILabel!
#IBOutlet weak var booleanToggleSwitch: UISwitch!
}
Any takers?
I agree with the other contributors. The cells should not be used for data storage. You should consider another approach (like the one HMHero suggests).
But, as your question was also about how to access a UITableViewCell before it is removed, there is a method in UITableViewDelegate that you can use for that:
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// do something with the cell before it gets deallocated
}
This method tells the delegate that the specified cell was removed from the table. So it gives a last chance to do something with that cell before it disappears.
Because of table view reuses its cells, usually, it's not a good idea if your data depends on some components from the table view cell. Rather, it should be the other way around. Your table view data always drive it's table view cell's component even before any user input data is provided in your case.
Initial Data - your should already have somewhere in your code. I created my own from your provided code
let data = CellData()
data.title = "Troffer Light Fittin"
data.value = false
let data2 = CellData()
data2.title = "Length Drop"
data2.value = "0"
cellData.append(data)
cellData.append(data2)
Example
enum CellType {
case numericInput, booleanInput
}
class CellData {
var title: String?
var value: Any?
var cellType: CellType {
if let _ = value as? Bool {
return .booleanInput
} else {
return .numericInput
}
}
}
protocol DataCellDelegate: class {
func didChangeCellData(_ cell: UITableViewCell)
}
class DataTableViewCell: UITableViewCell {
var data: CellData?
weak var delegate: DataCellDelegate?
}
class NumericInputTableViewCell: DataTableViewCell {
let userInputTextField: UITextField = UITextField()
override var data: CellData? {
didSet {
textLabel?.text = data?.title
if let value = data?.value as? String {
userInputTextField.text = value
}
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
userInputTextField.addTarget(self, action: #selector(textDidChange(_:)), for: .editingChanged)
contentView.addSubview(userInputTextField)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func textDidChange(_ textField: UITextField) {
//update data and let the delegate know data is updated
data?.value = textField.text
delegate?.didChangeCellData(self)
}
//Disregard this part
override func layoutSubviews() {
super.layoutSubviews()
textLabel?.frame.size.height = bounds.size.height / 2
userInputTextField.frame = CGRect(x: (textLabel?.frame.origin.x ?? 10), y: bounds.size.height / 2, width: bounds.size.width - (textLabel?.frame.origin.x ?? 10), height: bounds.size.height / 2)
}
}
class BooleanInputTableViewCell: DataTableViewCell {
override var data: CellData? {
didSet {
textLabel?.text = data?.title
if let value = data?.value as? Bool {
booleanToggleSwitch.isOn = value
}
}
}
let booleanToggleSwitch = UISwitch(frame: .zero)
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
booleanToggleSwitch.addTarget(self, action: #selector(toggled), for: .valueChanged)
booleanToggleSwitch.isOn = true
accessoryView = booleanToggleSwitch
accessoryType = .none
selectionStyle = .none
}
func toggled() {
//update data and let the delegate know data is updated
data?.value = booleanToggleSwitch.isOn
delegate?.didChangeCellData(self)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In View Controller, you should update your original data source so when you scroll the table view, the data source privide right infomation.
func didChangeCellData(_ cell: UITableViewCell) {
if let cell = cell as? DataTableViewCell {
for data in cellData {
if let title = data.title, let titlePassed = cell.data?.title, title == titlePassed {
data.value = cell.data?.value
}
}
}
for data in cellData {
print("\(data.title) \(data.value)")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = cellData[indexPath.row]
let cell: DataTableViewCell
if data.cellType == .booleanInput {
cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(BooleanInputTableViewCell.self), for: indexPath) as! BooleanInputTableViewCell
} else {
cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(NumericInputTableViewCell.self), for: indexPath) as! NumericInputTableViewCell
}
cell.data = cellData[indexPath.row]
cell.delegate = self
return cell
}
In short, try to have a single data source for table view and use the delegate to pass the updated data in the cell back to the data source.
Please disregard anything that has to do with layout. I didn't use the storyboard to test your requirements.

get indexPath of UITableViewCell on click of Button from Cell

I have a button (red color cross) in the UITableViewCell and on click of that button I want to get indexPath of the UITableViewCell.
Right now I am assigning tag to each of the button like this
cell.closeButton.tag = indexPath.section
and the on click of the button I get the indexPath.section value like this:
#IBAction func closeImageButtonPressed(sender: AnyObject) {
data.removeAtIndex(sender.tag)
tableView.reloadData()
}
Is this the right way of implementation or is there any other clean way to do this?
Use Delegates:
MyCell.swift:
import UIKit
//1. delegate method
protocol MyCellDelegate: AnyObject {
func btnCloseTapped(cell: MyCell)
}
class MyCell: UICollectionViewCell {
#IBOutlet var btnClose: UIButton!
//2. create delegate variable
weak var delegate: MyCellDelegate?
//3. assign this action to close button
#IBAction func btnCloseTapped(sender: AnyObject) {
//4. call delegate method
//check delegate is not nil with `?`
delegate?.btnCloseTapped(cell: self)
}
}
MyViewController.swift:
//5. Conform to delegate method
class MyViewController: UIViewController, MyCellDelegate, UITableViewDataSource,UITableViewDelegate {
//6. Implement Delegate Method
func btnCloseTapped(cell: MyCell) {
//Get the indexpath of cell where button was tapped
let indexPath = self.collectionView.indexPathForCell(cell)
print(indexPath!.row)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCell") as! MyCell
//7. delegate view controller instance to the cell
cell.delegate = self
return cell
}
}
How to get cell indexPath for tapping button in Swift 4 with button selector
#objc func buttonClicked(_sender:UIButton){
let buttonPosition = sender.convert(CGPoint.zero, to: self.tableView)
let indexPath = self.tableView.indexPathForRow(at:buttonPosition)
let cell = self.tableView.cellForRow(at: indexPath) as! UITableViewCell
print(cell.itemLabel.text)//print or get item
}
Try with the best use of swift closures : Simple, Quick & Easy.
In cellForRowAtIndexPath method:
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCellIdentifier", for: indexPath) as! CustomCell
cell.btnTick.mk_addTapHandler { (btn) in
print("You can use here also directly : \(indexPath.row)")
self.btnTapped(btn: btn, indexPath: indexPath)
}
Selector Method for external use out of cellForRowAtIndexPath method:
func btnTapped(btn:UIButton, indexPath:IndexPath) {
print("IndexPath : \(indexPath.row)")
}
Extension for UIButton :
extension UIButton {
private class Action {
var action: (UIButton) -> Void
init(action: #escaping (UIButton) -> Void) {
self.action = action
}
}
private struct AssociatedKeys {
static var ActionTapped = "actionTapped"
}
private var tapAction: Action? {
set { objc_setAssociatedObject(self, &AssociatedKeys.ActionTapped, newValue, .OBJC_ASSOCIATION_RETAIN) }
get { return objc_getAssociatedObject(self, &AssociatedKeys.ActionTapped) as? Action }
}
#objc dynamic private func handleAction(_ recognizer: UIButton) {
tapAction?.action(recognizer)
}
func mk_addTapHandler(action: #escaping (UIButton) -> Void) {
self.addTarget(self, action: #selector(handleAction(_:)), for: .touchUpInside)
tapAction = Action(action: action)
}
}
In Swift 4 , just use this:
func buttonTapped(_ sender: UIButton) {
let buttonPostion = sender.convert(sender.bounds.origin, to: tableView)
if let indexPath = tableView.indexPathForRow(at: buttonPostion) {
let rowIndex = indexPath.row
}
}
You can also get NSIndexPath from CGPoint this way:
#IBAction func closeImageButtonPressed(sender: AnyObject) {
var buttonPosition = sender.convertPoint(CGPointZero, to: self.tableView)
var indexPath = self.tableView.indexPathForRow(atPoint: buttonPosition)!
}
Create a custom class of UIButton and declare a stored property like this and use it to retrieve assigned indexPath from callFroRowAtIndexPath.
class VUIButton: UIButton {
var indexPath: NSIndexPath = NSIndexPath()
}
This is the full proof solution that your indexPath will never be wrong in any condition. Try once.
//
// ViewController.swift
// Table
//
// Created by Ngugi Nduung'u on 24/08/2017.
// Copyright © 2017 Ngugi Ndung'u. All rights reserved.
//
import UIKit
class ViewController: UITableViewController{
let identifier = "cellId"
var items = ["item1", "2", "3"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.title = "Table"
tableView.register(MyClass.self, forCellReuseIdentifier: "cellId")
}
//Return number of cells you need
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! MyClass
cell.controller = self
cell.label.text = items[indexPath.row]
return cell
}
// Delete a cell when delete button on cell is clicked
func delete(cell: UITableViewCell){
print("delete")
if let deletePath = tableView.indexPath(for: cell){
items.remove(at: deletePath.row)
tableView.deleteRows(at: [deletePath], with: .automatic)
}
}
}
class MyClass : UITableViewCell{
var controller : ViewController?
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
let label : UILabel = {
let label = UILabel()
label.text = "My very first cell"
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let btn : UIButton = {
let bt = UIButton(type: .system)
bt.translatesAutoresizingMaskIntoConstraints = false
bt.setTitle("Delete", for: .normal)
bt.setTitleColor(.red, for: .normal)
return bt
}()
func handleDelete(){
controller?.delete(cell: self)
}
func setUpViews(){
addSubview(label)
addSubview(btn)
btn.addTarget(self, action: #selector(MyClass.handleDelete), for: .touchUpInside)
btn.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
label.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 16).isActive = true
label.widthAnchor.constraint(equalTo: self.widthAnchor , multiplier: 0.8).isActive = true
label.rightAnchor.constraint(equalTo: btn.leftAnchor).isActive = true
}
}
Here is a full example that will answer your question.
In your cellForRow:
#import <objc/runtime.h>
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
setAssociatedObject(object: YOURBUTTON, key: KEYSTRING, value: indexPath)
}
#IBAction func closeImageButtonPressed(sender: AnyObject) {
let val = getAssociatedObject(object: sender, key: KEYSTROKING)
}
Here val is your indexPath object, your can pass any object like you can assign pass cell object and get it in button action.
try this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = (tableView.dequeueReusableCell(withIdentifier: "MainViewCell", forIndexPath: indexPath) as! MainTableViewCell)
cell.myButton().addTarget(self, action: Selector("myClickEvent:event:"), forControlEvents: .touchUpInside)
return cell
}
this function get the position of row click
#IBAction func myClickEvent(_ sender: Any, event: Any) {
var touches = event.allTouches()!
var touch = touches.first!
var currentTouchPosition = touch.location(inView: feedsList)
var indexPath = feedsList.indexPathForRow(atPoint: currentTouchPosition)!
print("position:\(indexPath.row)")
}
class MyCell: UICollectionViewCell {
#IBOutlet weak var btnPlus: UIButton!
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
cell.btnPlus.addTarget(self, action: #selector(increment_Action(sender:)),
for: .touchUpInside)
cell.btnPlus.tag = indexPath.row
cell.btnPlus.superview?.tag = indexPath.section
}
#objc func increment_Action(sender: UIButton) {
let btn = sender as! UIButton
let section = btn.superview?.tag ?? 0
let row = sender.tag
}

Resources