I have created two expandable cells but the second one doesn't expand when I tap on it
I googled a lot of ways how to do this but this one seemed the shortest and the simplest one.
private var dateCellExpanded: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// For removing the extra empty spaces of TableView below
self.tableView!.tableFooterView = UIView()
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 0 {
if dateCellExpanded {
dateCellExpanded = false
} else {
dateCellExpanded = true
}
tableView.beginUpdates()
tableView.endUpdates()
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
if dateCellExpanded {
return 250
} else {
return 40
}
}
return 40
}
I expect to be able to add as many cells as I want and have them expand on tap, but only the first one does.
Here's what happens
It's only working for the first row because you're limiting it to the first row with if indexPath.row == 0.
You'll need an array of Bool rather than a single Bool as you need to track the state for all rows.
Just initialize the array to an array of false values with the count of your rows. Set NUMBER_OF_ROWS below accordingly.
Then, when selecting a cell, you flip (or toggle()) the boolean for that row and ask the table to update.
private var isCellExpanded: [Bool] = Array(repeating: false, count: NUMBER_OF_ROWS)
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
isCellExpanded[indexPath.row].toggle()
tableView.beginUpdates()
tableView.endUpdates()
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if isCellExpanded[indexPath.row] {
return 250
} else {
return 40
}
}
Note that this assumes that you're only using one section. Otherwise you'll have to adjust the data structure [Bool] to [[Bool]].
Related
I created UITableViewController to show reviews, shown in below picture.
It shows the layout correctly for the first build like below.
Correct Layout
But when I close the app and re-open it, It shows the layout like below.
Wrong Layout
I am using Xcode 12.0 and I tried on iOS 12,iOS 13 and iOS 14 same problem occurs.
Here is my code for the app: (It's a Table View Controller)
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addTapped(_ sender: Any) {
self.present(AlertService().ReviewAlert(), animated: true, completion: nil)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
tableView.layoutIfNeeded()
}
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 5
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ReviewCell", for: indexPath) as! reviewCell
// Configure the cell...
cell.ratingSetup(rating: (indexPath.row + 1))
cell.reviewLabel.text = String(repeating: "Lorem Ipsum ", count: ((indexPath.row + 1) * 5))
if indexPath.row == 1 || indexPath.row == 3 { cell.markedAsRead.isHidden = true }
cell.sizeToFit()
cell.reviewLabel.numberOfLines = 0
cell.layoutIfNeeded()
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return CGFloat(100)
} }
Here is my Table View Cell Content as well.
Remove methods:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
tableView.layoutIfNeeded()
}
...
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return CGFloat(100)
} }
Remove:
cell.sizeToFit()
cell.reviewLabel.numberOfLines = 0
cell.layoutIfNeeded()
Set numberOfLines right in the storyboard for the label.
Check cell size field in the storyboard to be empty.
Check if you have all constraints - try to make the cell longer in the storyboard and check how label is rendered there (add long text to check if new lines are added).
Check if when launched you have no constraint warnings, if you have, then find "label to bottom" constraint and set priority from 1000 to 999.
I don't know why it happened but found a solution.
I added all other view to a Header.xib file and used basic cell type for review text.
Then It worked. Probably Cell can't handle auto dimension when there is bunch of stuff going on the prototype cell.
I have a table view with two sections and want to allow rows to be re-ordered for just one of the sections. I've found a lot of information about this, but can't restrict it to the single section. Here is my entire code - it's very simple, with one table view. I've put this together from various sources - thanks to those who have contributed to this topic.
To reproduce this in Storyboard, add a table view to the view controller, add some constraints, set number of Prototype Cells to 1, and set its Identifier to 'cell'.
The table view has two sections - 'Fruit' and 'Flowers'. Press and hold on any cell allows that cell to be moved, so this part works ok.
I want to restrict it so that I can only move in the first section.
Also, if I'm dragging a cell and move it from one section to another, it gives an error and the program crashes. I'd like it just to reject the move and send the cell back to its original position.
Thanks for any help. Ian
import UIKit
import MobileCoreServices
var fruitList = ["Orange", "Banana", "Apple", "Blueberry", "Mango"]
var flowerList = ["Rose", "Dahlia", "Hydrangea"]
// this all works
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITableViewDragDelegate, UITableViewDropDelegate {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dragInteractionEnabled = true
tableView.dragDelegate = self
tableView.dropDelegate = self
tableView.dragInteractionEnabled = true
}
func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == 0 {
return "Fruit"
} else {
return "Flowers"
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return fruitList.count
} else {
return flowerList.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
if indexPath.section == 0 {
cell.textLabel?.text = fruitList[indexPath.row]
} else {
cell.textLabel?.text = flowerList[indexPath.row]
}
return cell
}
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
}
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
var string: String
if indexPath.section == 0 {
string = fruitList[indexPath.row]
} else {
string = flowerList[indexPath.row]
}
guard let data = string.data(using: .utf8) else { return [] }
let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypePlainText as String)
return [UIDragItem(itemProvider: itemProvider)]
}
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
}
}
Just in case anyone finds their way here, thanks to the link #Paulw11 has provided, this is the extra bit of code needed. Replace
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
}
with
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let string = fruitList[sourceIndexPath.row]
fruitList.remove(at: sourceIndexPath.row)
fruitList.insert(string, at: destinationIndexPath.row)
}
Obviously this is just a simple example program, but I've now adapted this code to a complex situation and it works perfectly.
I have a UITableViewController where I have cells that I want to hide.
What I'm currently doing is hiding the cells with heightForRowAt returning 0 and cellForRowAt returning a cell with isHidden = false. But since I am using this solution, I noticed the app was slower when I'm scrolling in my tableView.
// Currently returning a height of 0 for hidden cells
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let post = timeline?.postObjects?[indexPath.row], post.hidden ?? false {
return 0.0
}
return UITableView.automaticDimension
}
// And a cell with cell.isHidden = false (corresponding to identifier "hiddenCell")
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = timeline?.postObjects?[indexPath.row] {
if post.hidden ?? false {
return tableView.dequeueReusableCell(withIdentifier: "hiddenCell", for: indexPath)
} else {
return (tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell).with(post: post, timelineController: self, darkMode: isDarkMode())
}
}
}
I was thinking about why not apply a filter on the array to totally remove hidden cells of the tableView, but I don't know if filtering them each time is great for performances...
// Returning only the number of visible cells
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return timeline?.postObjects?.filter{!($0.hidden ?? false)}.count
}
// And creating cells for only visible rows
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = timeline?.postObjects?.filter{!($0.hidden ?? false)}[indexPath.row] {
return (tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell).with(post: post, timelineController: self, darkMode: isDarkMode())
}
}
What is the best option? Hiding cells when generating them (first) or exclude them of the list (second)?
I would recommend to let the table view data source methods to deal with a filtered version of timeline. However, do not do this in cellForRowAt method because we need to do it one time but not for each cell drawing.
So, what you could do is to declare filteredTimeline and do the filter one time in the viewDidLoad method (for instance):
class TableViewController: UIViewController {
// ...
var filteredTimeline // as the same type of `timeline`
override func viewDidLoad() {
// ...
filteredTimeline = timeline?.postObjects?.filter{!($0.hidden ?? false)}
// ...
}
// Returning only the number of visible cells
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredTimeline.count ?? 0
}
// And creating cells for only visible rows
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = filteredTimeline?.postObjects?.filter{!($0.hidden ?? false)}[indexPath.row] {
return (tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell).with(post: post, timelineController: self, darkMode: isDarkMode())
}
}
// ...
}
In case of there is a better place to filteredTimeline = timeline?.postObjects?.filter{!($0.hidden ?? false)} rather than viewDidLoad, you might need to call tableView.reloadData().
An alternative you could do:
if you think that you don't need the original timeline you could filter it itself:
timeline = timeline?.postObjects?.filter{!($0.hidden ?? false)}
tableView.reloadData()
and you will not need an extra filtered array.
Extra tip:
In case of returning 0.0 value in heightForRowAt method for a certain row, cellForRowAt will not even get called; For example:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return indexPath.row == 0 ?? 0.0 : 100.0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// ...
}
At this point, cellForRowAt should get called only one time because the height for the first row is 0.0.
There is no point of having cells with a size of 0. Your best bet is to filter your datasource, but my suggestion would be to keep two arrays at the same time.
But handle the filtering elsewhere then in the numberOfRowsInSection.
var filteredObjects = []
func filterObjects() {
filteredObjects = timeline?.postObjects?.filter{!($0.hidden ?? false)}
}
// Returning only the number of visible cells
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredObjects.count
}
// And creating cells for only visible rows
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let post = filteredObjects[indexPath.row] {
return (tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell).with(post: post, timelineController: self, darkMode: isDarkMode())
}
}
I don't know how you handle the filtering, but whenever you want to apply your filter you simply
filterObjects()
tableView.reloadData()
I am trying to make a FAQ view controller using table view, I need little bit fix in the UI. here is the display of my FAQ VC right now
(please ignore the red line)
as we can see, basically there are 2 row height, 80 & 160. if the row is tapped (selected) the row height will expand from 80 (yellow line) to 160 (purple line).
I want to make the row height under the last question is still 80 not 160. I have tried but I can't set the row below the last question. here is my code I use
class FAQVC: UIViewController {
#IBOutlet weak var retryButton: DesignableButton!
#IBOutlet weak var tableView: UITableView!
var FAQs = [FAQ]()
var selectedIndexs: [IndexPath: Bool] = [:]
override func viewDidLoad() {
super.viewDidLoad()
getFAQData()
}
}
extension FAQVC : UITableViewDelegate, UITableViewDataSource {
// MARK: - Table View Delegate and Datasource
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return FAQs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FAQCell") as! FAQCell
cell.FAQData = FAQs[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if self.cellIsSelected(indexPath: indexPath) {
return 160
} else {
return 80
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let isSelected = !self.cellIsSelected(indexPath: indexPath)
selectedIndexs[indexPath] = isSelected
tableView.beginUpdates()
tableView.endUpdates()
}
func cellIsSelected(indexPath: IndexPath) -> Bool {
if let number = selectedIndexs[indexPath] {
return number
} else {
return false
}
}
}
Please put this code in your viewDidLoad() method of your controller.
YOUR_TABLE_VIEW.tableFooterView = UIView(frame: .zero)
this will remove extra separators.
Quickest way to do this would be to add an extra empty cell at the end with row height 80
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return FAQs.count + 1
}
Also, make sure you make a change to cellForRowAt method to accommodate this :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row < FAQs.count {
let cell = tableView.dequeueReusableCell(withIdentifier: "FAQCell") as! FAQCell
cell.FAQData = FAQs[indexPath.row]
return cell
}
return UITableViewCell()
}
EDIT :
I just read that you don't really require separators after the last cell. In that case look here https://spin.atomicobject.com/2017/01/02/remove-extra-separator-lines-uitableview/
I have a tableview with different sections, I need to be able to multiselect from different sections, but the rows in each section should be able to select mutually exclusive wise. For eg: in below screenshot I should be able to select either Margarita or BBQ Chicken from Pizza and same for Deep dish pizza but I should be able to multiselect between Pizza section and Deep dish pizza
Below is my code so far, I was wondering what would be the best way to approach this.
let section = ["Pizza", "Deep dish pizza"]
let items = [["Margarita", "BBQ Chicken"], ["Sausage", "meat lovers"]]
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.section[section]
}
override func numberOfSections(in tableView: UITableView) -> Int {
return section.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath)
// Configure the cell...
cell.textLabel?.text = items[indexPath.section][indexPath.row]
return cell
}
You should create some data element to track the selection for each row.
I would suggest a dictionary of [Int:Int] where the key is the section and the value is the row.
When a row is selected, you can then easily check to see if another row is already selected in that section, and deselect it if required.
var rowSelections = [Int:Int]()
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let section = indexPath.section
if let row = self.rowSelections[section] {
tableView.deselectRow(at: IndexPath(row:row, section:section), animated: true)
}
self.rowSelections[section]=indexPath.row
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let section = indexPath.section
self.rowSelections[section]=nil
}
Ok, so I figured it out, I created a method that would loop and check through all the rows and called it in both tableview didselect and deselect
func updateTableViewSelections(selectedIndex:IndexPath)
{
for i in 0 ..< tableView.numberOfSections
{
for k in 0 ..< tableView.numberOfRows(inSection: i)
{
if let cell = tableView.cellForRow(at: IndexPath(row: k, section: i))
{
if sections.getType(index: i) == selectedIndex.section
{
if (selectedIndex.row == k && cell.isSelected)
{
cell.setSelected(cell.isSelected, animated: false)
}
else
{
cell.setSelected(false, animated: false)
}
}
}
}
}
}
First allow multiple selection:
yourTableView.allowsMultipleSelection = true
To get the select rows:
let selectedRows = tableView.indexPathsForSelectedRows
Then within the didselectrow function you can iterate through the selected rows and ensure that only 1 row within the section can be selected.