Facing issue in selecting and deselecting tableview cell in swift - ios

I am showing pincodes in tableview, and when i select a cell then it should select and if i tap on the same cell again then it should deselect(while tapping cell should work like switch)
but with below code
issue 1: initially i am unable to select 1st row but after selecting any other row and then able to select 1st row.. why? where am i wrong?
issue 2: only one time i can select deselect the same row with two tapping if i tap 3rd time continuously then unable to select the same row, why?.. please guide
class PincodeModel{
var name: String?
var id: Int?
var isSelected: Bool
init(name: String?, id: Int?, isSelected: Bool) {
self.name = name
self.id = id
self.isSelected = isSelected
}
}
class FilterViewController: UIViewController {
var pincodePreviousIndex: Int = -1
var pincodes = [PincodeModel]()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
for pincode in pincodeList {
self.pincodes.append(PincodeModel(name: pincode, id: 0, isSelected: false))
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SubFilterTableViewCell", for: indexPath) as! SubFilterTableViewCell
cell.title.text = self.pincodes[indexPath.row].name
if !self.pincodes.isEmpty {
if self.pincodes[indexPath.row].isSelected == true {
cell.tickImageView.image = #imageLiteral(resourceName: "iconTick")
}else {
cell.tickImageView.image = UIImage()
}
}
return cell
}
// EDITED Code according to below answer
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.pincodes[indexPath.row].isSelected = !self.pincodes[indexPath.row].isSelected
if self.pincodes[indexPath.row].isSelected == true {
self.filterData.pincode = pincodes[indexPath.row].name ?? ""
}else {
self.filterData.pincode = ""
}
if pincodePreviousIndex > 0 && pincodePreviousIndex != indexPath.row {
pincodes[pincodePreviousIndex].isSelected = false
}
pincodePreviousIndex = indexPath.row
}
this is working as i want when i select from index = 1, but if i select first row(index = 0) then the right mark remains if i select another row, why?
o/p with edited code:

For issue 1 - By using this line of code:
var pincodePreviousIndex: Int = 0
You cannot click the first row until you click another since
pincodes[pincodePreviousIndex].isSelected = false
Since you're defaulting to 0 in the beginning, that correlates to the first row.
For issue 2 - if you select row 2 (selected) and then select it again to deselect it: pincodePreviousIndex will hold the value of that row and then deselect it again with
pincodes[pincodePreviousIndex].isSelected = false
So even though you're selecting it it will deselect it.
I would do this at the top:
var pincodePreviousIndex: Int = -1
and at the bottom:
if pincodePreviousIndex > 0 && pincodePreviousIndex != indexPath.row {
pincodes[pincodePreviousIndex].isSelected = false
}

There are a couple approaches you can take to save yourself some trouble.
First, set .selectionStyle = .none on your cells, and then in your cell class, override setSelected(...). For example, I added an image view to my cell and gave it an empty-box as its image, and a checked-box as its highlighted image:
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
imgView.isHighlighted = selected ? true : false
}
Now the cell appearance will reflect its selected state which is maintained by the table view.
Next, instead of didSelectRowAt, we'll implement willSelectRowAt ... if the cell is currently selected, we'll deselect it (and update our data):
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
// is a row already selected?
if let idx = tableView.indexPathForSelectedRow {
if idx == indexPath {
// tapped row is already selected, so
// deselect it
tableView.deselectRow(at: indexPath, animated: false)
// update our data
pincodes[indexPath.row].isSelected = false
// tell table view NOT to select the row
return nil
} else {
// some other row is selected, so
// update our data
// table view will automatically deselect that row
pincodes[idx.row].isSelected = false
}
}
// tapped row should now be selected, so
// update our data
pincodes[indexPath.row].isSelected = true
// tell table view TO select the row
return indexPath
}
Here's a complete example:
class PincodeModel{
var name: String?
var id: Int?
var isSelected: Bool
init(name: String?, id: Int?, isSelected: Bool) {
self.name = name
self.id = id
self.isSelected = isSelected
}
}
class SelMethodTableViewController: UIViewController {
var pincodes: [PincodeModel] = []
let tableView = UITableView()
let infoView: UIView = {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
return v
}()
let infoTitle: UILabel = {
let v = UILabel()
v.text = "Info:"
return v
}()
let infoLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0..<20 {
let pcm = PincodeModel(name: "\(i)", id: i, isSelected: false)
pincodes.append(pcm)
}
[tableView, infoView, infoTitle, infoLabel].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
[infoTitle, infoLabel].forEach { v in
infoView.addSubview(v)
}
[tableView, infoView].forEach { v in
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain the table view on right-side of view
tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
tableView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.5),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
tableView.bottomAnchor.constraint(equalTo: infoView.topAnchor, constant: -16.0),
// let's add a tappable "info" view below the table view
infoView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
infoView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
infoView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
infoView.heightAnchor.constraint(equalToConstant: 120.0),
// add labels to infoView
infoTitle.topAnchor.constraint(equalTo: infoView.topAnchor, constant: 8.0),
infoTitle.leadingAnchor.constraint(equalTo: infoView.leadingAnchor, constant: 8.0),
infoTitle.trailingAnchor.constraint(equalTo: infoView.trailingAnchor, constant: -8.0),
infoLabel.topAnchor.constraint(equalTo: infoTitle.bottomAnchor, constant: 8.0),
infoLabel.leadingAnchor.constraint(equalTo: infoView.leadingAnchor, constant: 8.0),
infoLabel.trailingAnchor.constraint(equalTo: infoView.trailingAnchor, constant: -8.0),
//infoLabel.bottomAnchor.constraint(lessThanOrEqualTo: infoView.bottomAnchor, constant: -8.0),
])
tableView.dataSource = self
tableView.delegate = self
tableView.register(MyToggleCell.self, forCellReuseIdentifier: "toggleCell")
// just so we can see the frame of the table view
tableView.layer.borderWidth = 1.0
tableView.layer.borderColor = UIColor.red.cgColor
let t = UITapGestureRecognizer(target: self, action: #selector(showInfo(_:)))
infoView.addGestureRecognizer(t)
infoView.isUserInteractionEnabled = true
}
#objc func showInfo(_ g: UIGestureRecognizer) -> Void {
var s: String = ""
let selectedFromData = pincodes.filter( {$0.isSelected == true} )
s += "Data reports:"
if selectedFromData.count > 0 {
selectedFromData.forEach { ob in
let obID = ob.id ?? -1
s += " \(obID)"
}
} else {
s += " Nothing selected"
}
s += "\n"
s += "Table reports: "
if let selectedFromTable = tableView.indexPathsForSelectedRows {
selectedFromTable.forEach { idx in
s += " \(idx.row)"
}
} else {
s += " No rows selected"
}
infoLabel.text = s
}
}
extension SelMethodTableViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pincodes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "toggleCell", for: indexPath) as! MyToggleCell
c.label.text = pincodes[indexPath.row].name
c.selectionStyle = .none
return c
}
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
// is a row already selected?
if let idx = tableView.indexPathForSelectedRow {
if idx == indexPath {
// tapped row is already selected, so
// deselect it
tableView.deselectRow(at: indexPath, animated: false)
// update our data
pincodes[indexPath.row].isSelected = false
// tell table view NOT to select the row
return nil
} else {
// some other row is selected, so
// update our data
// table view will automatically deselect that row
pincodes[idx.row].isSelected = false
}
}
// tapped row should now be selected, so
// update our data
pincodes[indexPath.row].isSelected = true
// tell table view TO select the row
return indexPath
}
}
class MyToggleCell: UITableViewCell {
let imgView: UIImageView = {
let v = UIImageView()
return v
}()
let label: UILabel = {
let v = UILabel()
return v
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
[imgView, label].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(v)
}
let g = contentView.layoutMarginsGuide
// give bottom anchor less-than-required
// to avoid auto-layout complaints
let b = imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -4.0)
b.priority = .required - 1
NSLayoutConstraint.activate([
imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: 4.0),
imgView.widthAnchor.constraint(equalToConstant: 32.0),
imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
b,
label.centerYAnchor.constraint(equalTo: g.centerYAnchor, constant: 0.0),
label.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 16.0),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
])
if let img1 = UIImage(systemName: "square"),
let img2 = UIImage(systemName: "checkmark.square") {
imgView.image = img1
imgView.highlightedImage = img2
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
imgView.isHighlighted = selected ? true : false
}
}
It will look like this:
When running:
Tapping a row will select that row
Tapping a different row will select the new row and deselect the currently selected row
Tapping the already-selected row will deselect it
Tapping the gray "info view" will report on the selection states from both the data and the table view
Note that if a selected row is scrolled out-of-view, it will remain selected (and will show selected when scrolled back into view) and the data and table view selection states will continue to be correct.
Edit
If we want to use didSelectRowAt (perhaps for other uses), we can "toggle" the selected row like this:
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
// if the tapped row is already selected
if let indexPathForSelectedRow = tableView.indexPathForSelectedRow,
indexPathForSelectedRow == indexPath {
tableView.deselectRow(at: indexPath, animated: false)
// calling .deselectRow(at: ...) does NOT trigger a call to didDeselectRowAt
// so update our data here
pincodes[indexPath.row].isSelected = false
return nil
}
return indexPath
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("did select", indexPath)
pincodes[indexPath.row].isSelected = true
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
print("did deselect", indexPath)
pincodes[indexPath.row].isSelected = false
}

Related

Deselect the rest of cells when first cell is select

I have a tableview cells, and the titles I get from server. Titles are array of strings. The data is drawing like this
All data
List item
Coffees
Teas
Dessert
Main dishes
when I click on each of these I can easily select them and deselect, that part works perfect. Now i want that when i select List item, Coffees, teas, dessert, main dishes together, these 5 items will be deselected and all data(first cell) will be selected automatically.
I know i should write this in didselect method, only i dont know how to write logic correctly. Please help me to solve this. Thanks
this is my code example
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let isSelected = arrayOfTitles[indexPath.row].isSelected else {arrayOfTitles[indexPath.row].isSelected = true; return}
arrayOfTitles[indexPath.row].isSelected = !isSelected
filter?.values?[indexPath.row].isSelected = !isSelected
for i in 1..<arrayOfTitles.count {
if arrayOfTitles[i].isSelected == isSelected {
arrayOfTitles[0].isSelected = isSelected
arrayOfTitles[i].isSelected = !isSelected
}
}
}
You can try
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let isSelected = arrayOfTitles[indexPath.row].isSelected else {arrayOfTitles[indexPath.row].isSelected = true; return}
arrayOfTitles[indexPath.row].isSelected = !isSelected
filter?.values?[indexPath.row].isSelected = !isSelected
let allSelected = arrayOfTitles[1...].filter { $0.isSelected }
if allSelected.count == arrayOfTitles.count - 1 {
arrayOfTitles[0].isSelected = true
(1..<arrayOfTitles.count).forEach { arrayOfTitles[$0].isSelected = false }
}
}
Because a table view maintains its own array of selected rows (IndexPaths), you don't really need to maintain a separate .isSelected property on your data. If you need it (for state persistence, for example), you can always grab the selected rows when needed.
So, here's another approach...
I'm guessing that if you have some of the rows selected, and you then select the top row, you would also want to deselect the other rows:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var arrayOfTitles: [String] = [
"All data",
"List Item",
"Coffees",
"Teas",
"Dessert",
"Main dishes",
]
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "c")
tableView.dataSource = self
tableView.delegate = self
tableView.allowsMultipleSelection = true
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrayOfTitles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath)
c.textLabel?.text = arrayOfTitles[indexPath.row]
return c
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// ignore if nothing is selected
guard let paths = tableView.indexPathsForSelectedRows else { return }
// if we selected the top row ("All data"), deselect the others
if indexPath.row == 0 {
// deselect the other rows
paths.forEach {
if $0.row != 0 {
tableView.deselectRow(at: $0, animated: true)
}
}
} else {
// get the selected rows as a sorted array of Int
let rows: [Int] = (paths.map { $0.row }).sorted()
// Int array of rows 1 thru 5
let myFiveRows: [Int] = Array(1...5)
// if the two arrays are equal (they're both [1,2,3,4,5])
if rows == myFiveRows {
// deselect all
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
// select the top row
tableView.selectRow(at: IndexPath(row: 0, section: 0), animated: true, scrollPosition: .none)
}
}
}
}

Can't combine UIImageView rotation animation and tableView section reload

I have 4 sections, each section have 2 nested rows. I open the rows by tapping on each section.
Here is how my initial data looks like. It has title, subtitle and options (which is what nested rows should display):
private var sections = [
SortingSection(title: "По имени", subtitle: "Российский рубль", options: ["По возрастанию (А→Я)", "По убыванию (Я→А)"]),
SortingSection(title: "По короткому имени", subtitle: "RUB", options: ["По возрастанию (А→Я)", "По убыванию (Я→А)"]),
SortingSection(title: "По значению", subtitle: "86,22", options: ["По возрастанию (1→2)", "По убыванию (2→1)"]),
SortingSection(title: "Своя", subtitle: "в любом порядке", options: ["Включить"])
]
When I tap on a section I want it accessory (chevron.right, made as UIImageView) be rotated in sync with expanding of nested rows and when I click again the same behaviour for closing.
I have a variable called isOpened (bool, false by default), which I change from false to true and back each tap in didSelectRowAt. Based on that a show all nested cells and rotate the UIImageView:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
sections[indexPath.section].isOpened.toggle()
guard let cell = tableView.cellForRow(at: indexPath) as? MainSortTableViewCell else { return }
UIView.animate(withDuration: 0.3) {
if self.sections[indexPath.section].isOpened {
cell.chevronImage.transform = CGAffineTransform(rotationAngle: .pi/2)
} else {
cell.chevronImage.transform = .identity
}
} completion: { _ in
tableView.reloadSections([indexPath.section], with: .none)
}
}
As you can see above I reload tableView section to show\hide nested rows in a completion block after animation. I can't use reloadSections in an if\else statement because then chevron animation gets skipped.
Also my numberOrRowsInSection method:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = sections[section]
if section.isOpened {
return section.options.count + 1
} else {
return 1
}
}
Here is how it looks now: CLICK
Here is what I want (any iPhone native apps):CLICK
I tried to add and delete rows instead of reloading the whole section, but always end up with error:
UIView.animate(withDuration: 0.3) {
if self.sections[indexPath.section].isOpened {
cell.chevronImage.transform = CGAffineTransform(rotationAngle: .pi/2)
for i in 0..<self.sections[indexPath.section].options.count {
tableView.insertRows(at: [IndexPath(row: 1+i, section: indexPath.section)], with: .none)
}
} else {
cell.chevronImage.transform = .identity
for i in 0..<self.sections[indexPath.section].options.count {
tableView.deleteRows(at: [IndexPath(row: i-1, section: indexPath.section)], with: .none)
}
}
}
How can I change my code to solve the task and animate chevron at the same time nested rows expand or close?
As you've seen, if you want to animate an element in a cell you cannot do so at the same time as reloading the cell.
So, to get the effect you want, one approach will be to split your data into "section pairs."
So, instead of this:
you'll have this:
When tapping on a "header" section, you can animate the image view rotation for that cell while reloading the next section.
It takes a little more management of the data -- but, really, not that much.
For example, if the data structure is:
struct SortingSection {
var title: String = ""
var subtitle: String = ""
var options: [String] = []
var isOpened: Bool = false
}
in numberOfSections we can return sections.count * 2
Then, in numberOfRowsInSection, we'll get the "virtualSection" number to get the index into our data array - something like this:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let virtualSection: Int = section / 2
let secItem = sections[virtualSection]
if section % 2 == 0 {
return 1
}
if secItem.isOpened {
return secItem.options.count
}
return 0
}
similarly, in cellForRowAt:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let virtualSection: Int = indexPath.section / 2
let secItem = sections[virtualSection]
if indexPath.section % 2 == 0 {
// return a "header row cell"
}
// return a "option row cell"
}
and finally, in didSelectRowAt:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let virtualSection: Int = indexPath.section / 2
// if it's a "header row"
if indexPath.section % 2 == 0 {
sections[virtualSection].isOpened.toggle()
guard let c = tableView.cellForRow(at: indexPath) as? ExpandCell else { return }
UIView.animate(withDuration: 0.3) {
if self.sections[virtualSection].isOpened {
c.chevronImageView.transform = CGAffineTransform(rotationAngle: .pi/2)
} else {
c.chevronImageView.transform = .identity
}
// reload the NEXT section
tableView.reloadSections([indexPath.section + 1], with: .automatic)
}
}
}
Here's a complete implementation to try out. Everything is done via code (no #IBOutlet connections), so create a new UITableViewController and assign its custom class to ExpandSectionTableViewController:
struct SortingSection {
var title: String = ""
var subtitle: String = ""
var options: [String] = []
var isOpened: Bool = false
}
class ExpandCell: UITableViewCell {
let titleLabel = UILabel()
let subtitleLabel = UILabel()
let chevronImageView = UIImageView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(titleLabel)
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(subtitleLabel)
chevronImageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(chevronImageView)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: g.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4.0),
subtitleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
chevronImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
chevronImageView.widthAnchor.constraint(equalToConstant: 40.0),
chevronImageView.heightAnchor.constraint(equalTo: chevronImageView.widthAnchor),
chevronImageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
subtitleLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
subtitleLabel.font = .systemFont(ofSize: 12.0, weight: .regular)
subtitleLabel.textColor = .gray
chevronImageView.contentMode = .center
let cfg = UIImage.SymbolConfiguration(pointSize: 24.0, weight: .regular)
if let img = UIImage(systemName: "chevron.right", withConfiguration: cfg) {
chevronImageView.image = img
}
}
}
class SubCell: UITableViewCell {
let titleLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
titleLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(titleLabel)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: g.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
titleLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
titleLabel.font = .italicSystemFont(ofSize: 15.0)
}
}
class ExpandSectionTableViewController: UITableViewController {
var sections: [SortingSection] = []
override func viewDidLoad() {
super.viewDidLoad()
let optCounts: [Int] = [
2, 3, 2, 5, 4, 2, 2, 3, 3, 4, 2, 1, 2, 3, 4, 3, 2
]
for (i, val) in optCounts.enumerated() {
var opts: [String] = []
for n in 1...val {
opts.append("Section \(i) - Option \(n)")
}
sections.append(SortingSection(title: "Title \(i)", subtitle: "Subtitle \(i)", options: opts, isOpened: false))
}
tableView.register(ExpandCell.self, forCellReuseIdentifier: "expCell")
tableView.register(SubCell.self, forCellReuseIdentifier: "subCell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count * 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let virtualSection: Int = section / 2
let secItem = sections[virtualSection]
if section % 2 == 0 {
return 1
}
if secItem.isOpened {
return secItem.options.count
}
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let virtualSection: Int = indexPath.section / 2
let secItem = sections[virtualSection]
if indexPath.section % 2 == 0 {
let c = tableView.dequeueReusableCell(withIdentifier: "expCell", for: indexPath) as! ExpandCell
c.titleLabel.text = secItem.title
c.subtitleLabel.text = secItem.subtitle
c.chevronImageView.transform = secItem.isOpened ? CGAffineTransform(rotationAngle: .pi/2) : .identity
c.selectionStyle = .none
return c
}
let c = tableView.dequeueReusableCell(withIdentifier: "subCell", for: indexPath) as! SubCell
c.titleLabel.text = secItem.options[indexPath.row]
return c
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let virtualSection: Int = indexPath.section / 2
// if it's a "header row"
if indexPath.section % 2 == 0 {
sections[virtualSection].isOpened.toggle()
guard let c = tableView.cellForRow(at: indexPath) as? ExpandCell else { return }
UIView.animate(withDuration: 0.3) {
if self.sections[virtualSection].isOpened {
c.chevronImageView.transform = CGAffineTransform(rotationAngle: .pi/2)
} else {
c.chevronImageView.transform = .identity
}
// reload the NEXT section
tableView.reloadSections([indexPath.section + 1], with: .automatic)
}
}
}
}

Saving TableView cells data (containing segment controller) using button

I'm tired of searching about what I want, so I will ask here and hope you guys help if possible.
I have a tableview contain segments in each cell and I want to save all cells segment using button [outside the tableview] so I can show them in another table later.
Here is my tableview
here is my view Controller:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
let arr = ["item 1",
"item 2",
"item 3"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomCell.self, forCellReuseIdentifier: "cell")
}
#IBAction func saveButtonAction(_ sender: UIButton) {
// I want to save the cells segmented control selectedSegmentIndex???
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
90
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arr.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
cell.textView.text = arr[indexPath.row]
return cell
}
}
and here is my Custom Cell and it contain TextView and Segment Controller I tried to save the segments changes in array but I don't know what to do after lol:
import UIKit
class CustomCell: UITableViewCell {
let textView: UITextView = {
let tv = UITextView()
tv.translatesAutoresizingMaskIntoConstraints = false
tv.font = UIFont.systemFont(ofSize: 16)
tv.isEditable = false
return tv
}()
// var rowIndexPath: Int?
var segmentArray: [Int] = []
lazy var itemSegmentedControl: UISegmentedControl = {
let sc = UISegmentedControl(items: ["1", "2", "3"])
sc.translatesAutoresizingMaskIntoConstraints = false
sc.tintColor = UIColor.white
sc.selectedSegmentIndex = 0
sc.addTarget(self, action: #selector(handlePointChange), for: .valueChanged)
return sc
}()
let stackView: UIStackView = {
let sv = UIStackView()
sv.translatesAutoresizingMaskIntoConstraints = false
return sv
}()
#objc func handlePointChange() {
let segChange = itemSegmentedControl.titleForSegment(at: itemSegmentedControl.selectedSegmentIndex)!
segmentArray.append(Int(segChange)!)
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
stackView.addSubview(textView)
stackView.addSubview(itemSegmentedControl)
addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
stackView.widthAnchor.constraint(equalToConstant: 400).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 85).isActive = true
itemSegmentedControl.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
itemSegmentedControl.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 5).isActive = true
itemSegmentedControl.widthAnchor.constraint(equalTo: textView.widthAnchor).isActive = true
itemSegmentedControl.heightAnchor.constraint(equalToConstant: 40).isActive = true
textView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -10).isActive = true
textView.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
textView.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = true
textView.heightAnchor.constraint(equalToConstant: 40).isActive = true
}
}
Regards and thanks.
First: in the ViewController I added the empty array:
// I moved the segment array from the custom cell class to here
var segmentArray: [Int:Int] = [:]
override func viewDidLoad() {
super.viewDidLoad()
// I made this array to store default state to the items segments
segmentArray = [0:0,1:0,2:0]
}
// with this func I can update the segmentArray
func getSegmentNumber(r: Int, s: Int) {
segmentArray[r] = Int(s)
}
#IBAction func saveButtonAction(_ sender: UIButton) {
// Here I can save the new state for the segment to the firebase or anywhere.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
cell.textView.text = arr[indexPath.row]
// here to store the index path row
cell.rowIndexPath = indexPath.row
// you need to add the link because it won't work
cell.link = self
return cell
}
Second: in the Custom Cell Class:
// I removed the store array
var segmentArray: [Int] = [] // removed
// I made link to the ViewController
var link: ViewController?
// variable to get the index path row
var rowIndexPath: Int?
// here I can update the state of the segment using the link
#objc func handlePointChange() {
let segChange = itemSegmentedControl.selectedSegmentIndex
link?.getSegmentNumber(r: rowIndexPath!, s: segChange)
}
That worked for me, and I hope you got it.
If you need anything you can ask.
Again thanks to Kudos.
Regards to all

How to synchronise events on the selection of a textview in a custom cell?

I have a textView, in a custom cell, where when I begin editing I would like buttons to appear in the cell. To do this I have created a variable 'selectedIndexPathRow' to which I assign the indexPath.row of the textView in the 'textViewDidBeginEditing' function.
func textViewDidBeginEditing(_ textView: UITextView) {
let touchPosition:CGPoint = textView.convert(CGPoint.zero, to: self.createStoryTableView)
let indexPath = self.createStoryTableView.indexPathForRow(at: touchPosition)
self.selectedIndexPathRow = indexPath?.row ?? 0
.....
After which in my cellForRowAt I can show or hide the buttons according to whether the indexPathRow matches the variable. As follows:
if indexPath.row == selectedIndexPathRow {
cell.inputTextView.isEditable = true
cell.createBtnStack.isHidden = false
} else {
cell.createBtnStack.isHidden = true
}
The problem is that the buttons only appear after the editing has completed (on pressing enter or clicking outside of textView) and not at the same as when the editing begins (or when the textView is clicked).
To rectify this I have tried the following:
putting in tableview.reloaddata() into the 'didBeginEditing'. This produces the buttons synchronously but causes the textview to freeze.
using the 'textViewShouldBeginEditing' function as follows to try to pre-set the selectIndexPathRow variable:
func textViewShouldBeginEditing(_
textView: UITextView) -> Bool {
let touchPosition:CGPoint =
textView.convert(CGPoint.zero, to:
self.createStoryTableView)
let indexPath =
self.createStoryTableView.indexPathForRow(at: touchPosition)
self.selectedIndexPathRow = indexPath?.row ?? 0
return true
}
But the result of this is not consistent - sometimes the textview can be edited directly after clicking with no buttons showing up, sometimes the buttons show up but you have to click again to edit the textView.
Tell me if you have any suggestions on how to solve this.
Have the cell manage its own editing state by setting it to be the delegate of its textfield. Here is an example playground:
import UIKit
import PlaygroundSupport
class Cell: UITableViewCell {
let textView = UITextView()
let button = UIButton()
lazy var stackView: UIStackView = {
let stack = UIStackView(arrangedSubviews: [textView, button])
stack.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.heightAnchor.constraint(equalToConstant: 120),
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12),
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12)
])
button.setTitle("Button", for: .normal)
button.addTarget(self, action: #selector(tapButton), for: .touchUpInside)
textView.delegate = self
return stack
}()
func configure() {
contentView.backgroundColor = .gray
_ = stackView
button.isHidden = !textView.isFirstResponder
}
#objc private func tapButton() {
contentView.endEditing(true)
}
}
extension Cell: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
button.isHidden = false
}
func textViewDidEndEditing(_ textView: UITextView) {
button.isHidden = true
}
}
class V: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(Cell.self, forCellReuseIdentifier: String(describing: Cell.self))
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: Cell.self), for: indexPath)
(cell as? Cell)?.configure()
return cell
}
}
PlaygroundPage.current.liveView = V()

UITableViewCell doesn't use autolayout height when using insert cell into table

Background
I use purelayout to programmatically create my UITableViewCells, following the instructions here, which basically states that you gotta set the top/bottom constraints on a cell, then use
self.tableView.rowHeight = UITableViewAutomaticDimension;
to get it right:
Problem
Everything works fine, except when I insert a new row into a tableView. I get this effect: https://youtu.be/eTGWsxwDAdk
To explain: as soon as I click on one of the tip cells, the table is supposed to insert a driver tip row. However you'll notice that wen i refresh the section (by clicking on a tip box), all the cells height inexplicably increases, but when i click on tip boxes again, they go back to their normal height
this is done with this code
self.tableView.beginUpdates()
self.tableView.reloadSections(IndexSet(integer:1), with: .automatic)
self.tableView.endUpdates()
this is the implementation of the cellfor row
// init table
self.tableView.register(OrderChargeTableViewCell.self,
forCellReuseIdentifier: OrderChargeTableViewCell.regularCellIdentifier)
self.tableView.register(OrderChargeTableViewCell.self,
forCellReuseIdentifier: OrderChargeTableViewCell.boldCellIdentifier)
self.tableView.estimatedRowHeight = 25
self.tableView.rowHeight = UITableViewAutomaticDimension
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: OrderChargeTableViewCell?
if (indexPath.row == filteredModel.count-1) {
cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.boldCellIdentifier,
for: indexPath) as? OrderChargeTableViewCell
} else if (indexPath.row < filteredModel.count) {
cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.regularCellIdentifier,
for: indexPath) as? OrderChargeTableViewCell
}
// add data to cell labels
return cell!
}
and this is the code for the UITableViewCell itself:
final class OrderChargeTableViewCell: UITableViewCell {
// MARK: - init
static let boldCellIdentifier = "TTOrderDetailBoldTableViewCell"
static let regularCellIdentifier = "TTOrderDetailRegularTableViewCell"
private var didSetupConstraints = false
..
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
self.keyLabel = TTRLabel()
self.valueLabel = TTRLabel()
if (reuseIdentifier == OrderChargeTableViewCell.regularCellIdentifier) {
self.isCellStyleBold = false
} else if (reuseIdentifier == OrderChargeTableViewCell.boldCellIdentifier) {
self.isCellStyleBold = true
} else {
self.isCellStyleBold = false
assertionFailure( "Attempt to create an OrderCharge cell with the wrong cell identifier: \(String(describing: reuseIdentifier))")
}
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
contentView.addSubview(valueLabel)
}
override func updateConstraints()
{
if !didSetupConstraints {
if (isCellStyleBold) {
self.applyBoldFormatting()
} else {
self.applyRegularFormatting()
}
didSetupConstraints = true
}
super.updateConstraints()
}
public func applyBoldFormatting() {
keyLabel.font = .ttrSubTitle
valueLabel.font = .ttrBody
keyLabel.autoPinEdge(.leading, to: .leading, of: contentView, withOffset: 15)
keyLabel.autoAlignAxis(.vertical, toSameAxisOf: contentView)
keyLabel.autoPinEdge(.top, to: .top, of: contentView, withOffset: 8)
keyLabel.autoPinEdge(.bottom, to: .bottom, of: contentView, withOffset: -8)
valueLabel.autoPinEdge(.trailing, to: .trailing, of: contentView, withOffset: -15)
valueLabel.autoAlignAxis(.baseline, toSameAxisOf: keyLabel)
}
public func applyRegularFormatting() {
keyLabel.font = .ttrCaptions
valueLabel.font = TTRFont.Style.standard(.h3).value
keyLabel.autoPinEdge(.leading, to: .leading, of: contentView, withOffset: 15)
keyLabel.autoAlignAxis(.vertical, toSameAxisOf: contentView)
keyLabel.autoPinEdge(.top, to: .top, of: contentView, withOffset: 6)
keyLabel.autoPinEdge(.bottom, to: .bottom, of: contentView, withOffset: -4)
valueLabel.autoPinEdge(.trailing, to: .trailing, of: contentView, withOffset: -15)
valueLabel.autoAlignAxis(.baseline, toSameAxisOf: keyLabel)
}
the driver tip row that gets inserted has the standard 44 pixel height of a cell:
whereas the other (properly formatted) cells have the 25 height:
While the StackOverflow answer you followed has a lot of up-votes, it appears you took one bullet point which was not very well explained (and may be outdated), and I think that may be what's causing your problems.
You'll find many comments / posts / articles stating you should add your constraints in updateConstraints(), however, Apple's docs also state:
Override this method to optimize changes to your constraints.
Note
It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. For example, if you want to change a constraint in response to a button tap, make that change directly in the button’s action method.
You should only override this method when changing constraints in place is too slow, or when a view is producing a number of redundant changes.
I think you'll get much better results in what you're attempting if you add your subviews and their constraints when your cell is init'd.
Here is a simple example which has a similar layout to what you've shown. It creates a table with 2 sections - first section has a row with a "show/hide" button. When tapped, the second section will add/remove the "Driver Tip" row.
//
// InsertRemoveViewController.swift
//
// Created by Don Mag on 12/4/18.
//
import UIKit
struct MyRowData {
var title: String = ""
var value: CGFloat = 0.0
}
class OrderChargeTableViewCell: UITableViewCell {
static let boldCellIdentifier: String = "TTOrderDetailBoldTableViewCell"
static let regularCellIdentifier: String = "TTOrderDetailRegularTableViewCell"
var keyLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var valueLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(keyLabel)
contentView.addSubview(valueLabel)
let s = type(of: self).boldCellIdentifier
if self.reuseIdentifier == s {
NSLayoutConstraint.activate([
keyLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8.0),
keyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8.0),
keyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15.0),
valueLabel.centerYAnchor.constraint(equalTo: keyLabel.centerYAnchor, constant: 0.0),
valueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15.0),
])
keyLabel.font = UIFont.systemFont(ofSize: 15, weight: .bold)
valueLabel.font = UIFont.systemFont(ofSize: 15, weight: .bold)
} else {
NSLayoutConstraint.activate([
keyLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 6.0),
keyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4.0),
keyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15.0),
valueLabel.centerYAnchor.constraint(equalTo: keyLabel.centerYAnchor, constant: 0.0),
valueLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15.0),
])
keyLabel.font = UIFont.systemFont(ofSize: 12, weight: .bold)
valueLabel.font = UIFont.systemFont(ofSize: 12, weight: .regular)
}
}
}
class TipCell: UITableViewCell {
var callBack: (() -> ())?
var theButton: UIButton = {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Tap to Show/Hide Add Tip row", for: .normal)
b.setTitleColor(.blue, for: .normal)
b.backgroundColor = .yellow
return b
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
contentView.addSubview(theButton)
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20.0),
theButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
theButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: 0.0),
])
theButton.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
}
#objc func btnTapped(_ sender: Any?) -> Void {
callBack?()
}
}
class InsertRemoveViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var myData = [
MyRowData(title: "SUBTOTAL", value: 4),
MyRowData(title: "DELIVERY CHARGE", value: 1.99),
MyRowData(title: "DISCOUNT", value: -1.99),
MyRowData(title: "TOTAL", value: 4),
]
var tableView: UITableView = {
let v = UITableView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
func tipRowShowHide() {
let iPath = IndexPath(row: 3, section: 1)
if myData.count == 4 {
myData.insert(MyRowData(title: "DRIVER TIP", value: 2.0), at: 3)
tableView.insertRows(at: [iPath], with: .automatic)
} else {
myData.remove(at: 3)
tableView.deleteRows(at: [iPath], with: .automatic)
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(OrderChargeTableViewCell.self,
forCellReuseIdentifier: OrderChargeTableViewCell.regularCellIdentifier)
tableView.register(OrderChargeTableViewCell.self,
forCellReuseIdentifier: OrderChargeTableViewCell.boldCellIdentifier)
tableView.register(TipCell.self, forCellReuseIdentifier: "TipCell")
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 25
view.backgroundColor = .red
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 200.0),
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20.0),
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20.0),
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20.0),
])
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return " "
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return section == 0 ? 1 : myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "TipCell", for: indexPath) as! TipCell
cell.callBack = {
self.tipRowShowHide()
}
return cell
}
var cell: OrderChargeTableViewCell?
if indexPath.row == myData.count - 1 {
cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.boldCellIdentifier,
for: indexPath) as? OrderChargeTableViewCell
} else {
cell = tableView.dequeueReusableCell(withIdentifier: OrderChargeTableViewCell.regularCellIdentifier,
for: indexPath) as? OrderChargeTableViewCell
}
cell?.keyLabel.text = myData[indexPath.row].title
let val = myData[indexPath.row].value
cell?.valueLabel.text = String(format: "%0.02f USD", val)
return cell!
}
}
This is the result:
this is a brute force solution, one that i'm not proud of at all, but it's here for reference (won't mark it as correct answer):
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let orderChargesSection = self.getOrderChargesSection()
switch indexPath.section {
case orderChargesSection:
return self.getCellHeightForOrderCharges(row: indexPath.row)
default:
return UITableViewAutomaticDimension
}
}
private func getCellHeightForOrderCharges(row: Int) -> CGFloat {
let numRows = self.tableView(self.tableView, numberOfRowsInSection: self.getOrderChargesSection())
if (row == numRows - 1) {
return UITableViewAutomaticDimension
} else {
return 25.5
}
}
After beginUpdates / endUpdate and all of the inserts do this:
DispatchQueue.main.async {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}

Resources