UITableViewAutomaticDimension is making empty cells too big - ios

I'm making a TableView with UITableViewAutomaticDimension but for some reason the "extra" blank cells after the last one are the same dimensions as the last cell with text in it. I want the blank cells to all be a smaller size unless filled with text that requires the auto sizing to enlarge it. How can I change this? Any help would be appreciated. I'm very new to this. Thanks everybody! See below for a screenshot of the TableView. I've added my code for reference.
Import UIKit
class LoLFirstTableViewController: UITableViewController {
var tasks:[Task] = taskData
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 60.0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasks.count
}
#IBAction func cancelToLoLFirstTableViewController(_ segue:UIStoryboardSegue) {
}
#IBAction func saveAddTask(_ segue:UIStoryboardSegue) {
if let AddTaskTableViewController = segue.source as? AddTaskTableViewController {
if let task = AddTaskTableViewController.task {
tasks.append(task)
let indexPath = IndexPath(row: tasks.count-1, section: 0)
tableView.insertRows(at: [indexPath], with: .automatic)
}
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath)
as! TaskCell
let task = tasks[indexPath.row] as Task
cell.task = task
return cell
}
}

First, you set the default row height from storyboard. The empty row will be set to this height.
Then for the rows with texts, override this table view's delegate method:
optional func tableView(_ tableView: heightForRowAt indexPath: IndexPath) -> CGFloat
This will force the table view to return the height for the specified row. Get the text from the row by its index path. Calculate the text height and return the correct row height.
Here is the method to calculate a string's frame:
func boundingRect(with size: CGSize,
options: NSStringDrawingOptions = [],
attributes: [String : Any]? = nil,
context: NSStringDrawingContext?) -> CGRect

You can just hide the empty cells by adding this to viewDidLoad():
tableView.tableFooterView = UIView()

Related

Custom cells for expandable UiTableView

I have a data set that has an inner array and I have to show that Inner array in Expand collapse fashion.
For that I have designed 2 nib files. One for the sections, and other for the cell in sections.
I have attached UitableView and the delegated methods. I am successful to show the Header view, I am registering the header view like this.
let nib = UINib.init(nibName: "headerItemSavedListCell", bundle: nil)
self.lvSavedList.register(nib, forCellReuseIdentifier: "headerItemSavedListCell")
and for cell I am doing in the following method
if(indexPath.row == 0){
let header = Bundle.main.loadNibNamed("headerItemSavedListCell", owner: self, options: nil)?.first as! headerItemSavedListCell
return header
}else{
let cell = Bundle.main.loadNibNamed("ItemSavedListCell", owner: self, options: nil)?.first as! ItemSavedListCell
return cell
}
But its not working.
**So my questions is: **
How to load the inner cell view ?
How to expand collapse cell view that lies inside the Sections?
Please help if you have any tutorial regarding expandable Uitableview
the class i am using here are connected to xibs
make xib of view and bind below class
so first you have to make headerview like below
protocol HeaderDelegate:class{
func didSelectHeader(Header:HeaderFooter,at index:Int)
}
class HeaderFooter: UITableViewHeaderFooterView {
#IBOutlet weak var lblTitle: UILabel!
weak var delegate:HeaderDelegate?
var Expand = false
override func awakeFromNib() {
let tap = UITapGestureRecognizer.init(target: self, action: #selector(didSelect(_:)))
self.addGestureRecognizer(tap)
self.isUserInteractionEnabled = true
}
#objc func didSelect(_ tap:UITapGestureRecognizer)
{
delegate?.didSelectHeader(Header: self, at: self.tag)
}
override func prepareForReuse() {
Expand = false
}
}
above i added tap gesture to detect touch on headerViews
next make cell like below
class ExpandableCell: UITableViewCell {
var isExpanded = false
override func awakeFromNib() {
super.awakeFromNib()
isExpanded = false
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
in your view controller
tblView.register(UINib.init(nibName: "HeaderFooter", bundle: nil), forHeaderFooterViewReuseIdentifier: "HeaderFooter")
in tablview dataSorce and Delegate Method
func numberOfSections(in tableView: UITableView) -> Int {
return numberofsections
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberofrows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ExpandableCell") as? ExpandableCell else {
return UITableViewCell()
}
//configure cell here
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let header = tableView.headerView(forSection: indexPath.section) as? HeaderFooter
else {return 0}
if header.Expand
{
return UITableViewAutomaticDimension
}
else
{
return 0
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
return 50
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooter") as? HeaderFooter else {return nil}
//configure header View here
headerView.tag = section
return headerView
}
//MARK:-headerDelegate
func didSelectHeader(Header: HeaderFooter, at index: Int) {
Header.Expand = !Header.Expand
//comment below part if you dont want to collapse other rows when other section opened
for i in 0..<tblView.numberOfSections
{
if i != index
{
guard let header = tblView.headerView(forSection: i) as? HeaderFooter else {return}
header.Expand = false
for j in 0..<tblView.numberOfRows(inSection: i)
{
tblView.reloadRows(at: [IndexPath.init(row: j, section: i)], with: .automatic)
}
}
else
{
for j in 0..<tblView.numberOfRows(inSection: i)
{
tblView.reloadRows(at: [IndexPath.init(row: j, section: i)], with: .automatic)
}
}
}
}

expand and contract label not working in UITableViewCell

I am trying to use a UITableView and have cell contents which will expand or contract when the user clicks on the label.
However, the behavior I'm seeing is that the cell will contract (e.g. I am changing the label's numberOfLines from 0 to 1, and then the label will contract). However, when I change the label's numberOfLines from 1 to 0 it doesn't expand.
I put together a simple test program to show the issue I'm having.
I'm using a UITapGestureRecognizer to handle the tap event for the label, and that is where I expand or contract the label. Does anyone have any idea what I'm doing wrong?
Here's my storyboard and view controller code.
import UIKit
class MyCell : UITableViewCell {
#IBOutlet weak var myLabel: UILabel!
}
class TableViewController: UITableViewController {
let cellID = "cell"
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 75
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 12
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "section " + String(section)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 4
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: self.cellID, for: indexPath) as! MyCell
cell.myLabel.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleCellTapped(_:)))
cell.myLabel!.addGestureRecognizer(tapGesture)
// Configure the cell...
if indexPath.row % 2 == 0 {
cell.myLabel?.numberOfLines = 1
cell.myLabel.text = "This is some long text that should be truncated. It should not span over multiple lines. Let's hope this actually works. \(indexPath.row)"
} else {
cell.myLabel?.numberOfLines = 0
cell.myLabel.text = "This is some really, really long text. It should span over multiple lines. Let's hope this actually works. \(indexPath.row)"
}
return cell
}
#objc func handleCellTapped(_ sender: UITapGestureRecognizer) {
print("Inside handleCellTapped...")
guard let label = (sender.view as? UILabel) else { return }
//label.translatesAutoresizingMaskIntoConstraints = false
// expand or contract the cell accordingly
if label.numberOfLines == 0 {
label.numberOfLines = 1
} else {
label.numberOfLines = 0
}
}
}
Do two things.
Set the Vertical Content hugging priority and
Vertical Content compression resistance priority of the Label to 1000.
After changing the number of lines of the Label call the tableView.beginUpdates() and tableView.endUpdates()
This should work definitely.
You almost get it, but here is a couple of things you should care about.
First, handle the label by UIGestureRecognizer it's quite overhead. For that purposes UITableViewDelegate has own method:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
Second, you're using self-sizing cell, because of
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 75
There is one important rule for that: you should pin myLabel to each side of superview (see official docs why):
Last step, when the numberOfLines changed, you should animate cell's height (expand or collapse) without reloading the cell:
tableView.beginUpdates()
tableView.endUpdates()
Docs:
You can also use this method followed by the endUpdates() method to animate the change in the row heights without reloading the cell.
Full code:
class MyCell: UITableViewCell {
#IBOutlet weak var myLabel: UILabel!
}
class TableViewController: UITableViewController {
let cellID = "cell"
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 75
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 12
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "section " + String(section)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: self.cellID, for: indexPath) as! MyCell
cell.selectionStyle = .none // remove if you need cell selection
if indexPath.row % 2 == 0 {
cell.myLabel?.numberOfLines = 1
cell.myLabel.text = "This is some long text that should be truncated. It should not span over multiple lines. Let's hope this actually works. \(indexPath.row)"
} else {
cell.myLabel?.numberOfLines = 0
cell.myLabel.text = "This is some really, really long text. It should span over multiple lines. Let's hope this actually works. \(indexPath.row)"
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
guard let cell = tableView.cellForRow(at: indexPath) as? MyCell else { return }
cell.myLabel.numberOfLines = cell.myLabel.numberOfLines == 0 ? 1 : 0
tableView.beginUpdates()
tableView.endUpdates()
}
}
Try
tableView.beginUpdates()
if label.numberOfLines == 0 {
label.numberOfLines = 1
} else {
label.numberOfLines = 0
}
tableView.endUpdates()

Automatically change cell content based on content in cell - Xcode 9.2 Swift 4

I have a table View in a View Controller, and within the cells, my text gets cut off when it's too long. How do I get the cell to automatically change based on the content in the cell or get the text to wrap so the text doesn't get cut off? Here's an image of what I'm talking about.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
func allowMultipleLines(tableViewCell: UITableViewCell) {
tableViewCell.textLabel?.numberOfLines = 0
tableViewCell.textLabel?.lineBreakMode = .byWordWrapping
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return courses.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let course:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "course")!
course.textLabel?.text = courses[indexPath.row]
return course
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.performSegue(withIdentifier: "Courses", sender: nil)
}
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let detailView: DetailCoursesViewController = segue.destination as! DetailCoursesViewController
selectedRow = (table.indexPathForSelectedRow?.row)!
detailView.setCourseTitle (t: courses[selectedRow])
detailView.setCourseDescription (d: courseDescription[selectedRow])
detailView.setCredits (c: credits[selectedRow])
detailView.setPrerequisites (p: prereq[selectedRow])
}
Here is the image of the code
First you need to set leading, trailing, bottom and top constraints of label to contentView in the TableViewcell.
override func viewDidLoad() {
super.viewDidLoad()
self.tabelView.estimatedRowHeight = 50
self.tabelView.rowHeight = UITableViewAutomaticDimension
// Do any additional setup after loading the view.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return courses.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let course:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "course")!
cell.textLabel.numberOfLines = 0
course.textLabel?.text = courses[indexPath.row]
return course
}
1) Set UILabel property numberOfLines = 0 inside cellForRowAt of UITableView
2) Inside ViewDidLoad write below code
self.tabelView.estimatedRowHeight = 44
self.tabelView.rowHeight = UITableViewAutomaticDimension
Demo Example
1- Give a bottom constraint between the label and cell bottom like 10
2- In the function tableView(_:cellForRowAt:) do the following:
cell.textLabel.numberOfLines = 0
cell.textLabel.lineBreakMode = .byWordWrapping

iOS static table auto resize based on textView

I have Text views inside a static table. I want them to resize when there is need for a line break. How do i do this? This is my code so far.
override func viewDidLoad() {
super.viewDidLoad()
table.estimatedRowHeight = 40.0 // Replace with your actual estimation
table.rowHeight = UITableViewAutomaticDimension
// Tap to dismiss keyboard
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(EditInfoViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
}
func dismissKeyboard() {
view.endEditing(true)
// Save data
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
Swift 3 & Xcode 8.3.2
Use UILabel instead of UITextView, and set numberOfLine = 0, so it will automatic resize according to its content
or
if you want to keep UITextView instead UILabel, here is the code
class YourClass: UITableViewController, UITextViewDelegate {
var yourCustomCell: UITableViewCell = UITableViewCell()
override func viewDidLoad() {
super.viewDidLoad()
table.estimatedRowHeight = 40.0 // Replace with your actual estimation
table.rowHeight = UITableViewAutomaticDimension
// Tap to dismiss keyboard
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(EditInfoViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
// Add tableView delegate
tableView.dataSource = self
tableView.delegate = self
// Add textView delegate
yourTextView.delegate = self
}
// Text view delegate, dont forget to add yourTextView.delegate = self in viewDidLoad
func textViewDidChange(_ textView: UITextView) {
if textView == yourTextView {
let newHeight = yourCustomCell.frame.size.height + textView.contentSize.height
yourCustomCell.frame.size.height = newHeight
updateTableViewContentOffsetForTextView()
}
}
// Animate cell, the cell frame will follow textView content
func updateTableViewContentOffsetForTextView() {
let currentOffset = tableView.contentOffset
UIView.setAnimationsEnabled(false)
tableView.beginUpdates()
tableView.endUpdates()
UIView.setAnimationsEnabled(true)
tableView.setContentOffset(currentOffset, animated: false)
}
// UITableViewDelegate, UITableViewDataSource
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = yourCustomCell
cell.selectionStyle = .none
return cell
}
}
The result is here:
Result after using textViewDelegate, and custom resizing function

Get indexPath for cell of UITable embedded in UITableViewCell

So here is the structure of the TableView:
There is a main UITableView, and inside each UITableViewCell there is another UITableview
Screenshot:
Each of the UITableViewCells have been designed as Custom Views and have been added by loading it from the Nib in the cellForRowAtIndexPath function.
What I want to do is for any option selected in the inner Table Views I want to get the index path of the cell that the Table View is embeded in.
Document Layout:
I tried to follow the delegate approach mentioned by Paulw11 here:
swift: how to get the indexpath.row when a button in a cell is tapped?: StackOverflow
Note: The method suggested by Paulw11 works perfectly
Code(TableVC):
class TableVC: UITableViewController, QuestionCellDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 5
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 220.0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = Bundle.main.loadNibNamed("QuestionCell", owner: self, options: nil)?.first as! QuestionCell
cell.delegate = self
return cell
}
func sendCellInfo(cell: UITableViewCell) {
print(cell)
let indexPathForQuestion = tableView.indexPath(for: cell)
print(indexPathForQuestion)
}}
Code(QuestionCell):
protocol QuestionCellDelegate: class {
func sendCellInfo(cell: UITableViewCell)
}
class QuestionCell: UITableViewCell, UITableViewDelegate, UITableViewDataSource{
#IBOutlet weak var optionsTableview: UITableView!
var delegate: QuestionCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.optionsTableview.delegate = self
self.optionsTableview.dataSource = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = Bundle.main.loadNibNamed("OptionsCell", owner: self, options: nil)?.first as! OptionsCell
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedCell = optionsTableview.cellForRow(at: indexPath)
print("selectedCell")
self.delegate?.sendCellInfo(cell: selectedCell!)
}}
Code(OptionsCell):
class OptionsCell: UITableViewCell {
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
}}
Note: The current O/P is nil
Note: Code changed as per commented by pbasdf, realised the mistake
Found the solution due to pbasdf comment:
Delegate Function in TableVC:
func sendCellInfo(cell: UITableViewCell) {
/*** Take the cell passed and convert to a CGPoint wrt to TableView ***/
let cellPosition: CGPoint = cell.convert(cell.bounds.origin, to: self.tableView)
/*** wrt to CGPoint find index on current TableView ***/
/*** Returns as Section,Cell ***/
let indexPathForSelectedCell = (tableView.indexPathForRow(at: cellPosition)?.row)
print(indexPathForSelectedCell)
}
The following answer is added #Supratik Majumdar request for the logic which I said.
Supratik try using the following code, I hope you will get your need done.
//Initialize your question or answer in viewDidLoad or where ever you like to as shown below
self.questionArray = ["Question1", "Question2"]
self.optionArray = [["Option 1", "Option 2", "Option 3", "Option 4"], ["Option 1", "Option 2", "Option 3", "Option 4"]]
//Make us of the following tableview delegate & datasource code
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.questionArray.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.OptionArray[section].count
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String {
return self.questionArray[section]
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
let currentOptionArray = self.questionArray[section]
let currentOption = currentOptionArray[indexPath.row]
cell.textLabel.text = currentOption
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedIndex = indexPath
let selectedQuestionIndex = indexPath.section
let selectedOptionIndex = indexPath.row
//Make use of the data you need in the above values
}
Use this:
tableView.cellForRowAtIndexPath(YourIndexPath) as! OptionCell
You can do your own indexPath as global variable and filing it on didSelectRow method
YourIndexPath = indexPath

Resources