I'm having a lot of trouble with cell constraint modifications. I have a tableview with a nested cell inside of it and a button.
The bottom more button has a constraint to share the bottom of the cell. There is also a view hiding behind it with a height constraint, you can see it peeking out the bottom.
Basically if I click the the two constraints are changed and the cell is expanded.
TableViewCell:
#IBAction func expandPress(sender: AnyObject) {
if expandButtonBottomConstraint.active {
self.expandButtonBottomConstraint.active = false
self.descriptionTextHeightConstraint.constant =
self.descriptionTextHeightConstraint.constant * 10
self.tableView.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: buttonIndex, inSection: 0)], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
TableViewController
override func tableView(tableview: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let eventCell = tableView.dequeueReusableCellWithIdentifier("EventTableViewCell") as! EventTableViewCell
...
eventCell.buttonIndex = indexPath.row
....
self.tableView.layoutIfNeeded()
}
override func viewDidAppear(animated: Bool) {
self.tableView.reloadData()
}
If you click on the first event it expands, but if you click on on the third one, the third and the first expands. The index row is correctly the right row. How can I only expand a particular row consistently?
Related
I have a TableViewController which displays to-do list items. In the controller I have made a button which when pressed creates a new TableViewCell at the bottom which has a UITextView along with other elements.
Till now this is what I have managed to do -
Create a new cell upon button tap
Make the newly created cell's text view first responder
However, from what I have observed everything is working fine except when the last cell in the table is not visible, i.e., it is below the frame. In that case the cell gets created but is not made the first responder or some other cell's text view gets the cursor.
See the output here -
https://drive.google.com/file/d/1mRN8MEO5HBJ3ICUiRE0Yc4ib8tp62MYc/view?usp=sharing
Here is the code -
import UIKit
class TableViewController: UITableViewController, InboxCellDelegate {
var cell = InboxCell()
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.leftBarButtonItem = self.editButtonItem
tableView.register(UINib(nibName: "InboxCell", bundle: nil), forCellReuseIdentifier: "InboxCell")
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 50
}
#IBAction func inboxAddPressed(_ sender: UIBarButtonItem) {
addRowToEnd()
}
func addRowToEnd() {
Task.tasks.append("")
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: Task.tasks.count - 1, section: 0)], with: .automatic)
tableView.endUpdates()
cell.inboxTaskTextView.becomeFirstResponder()
}
func didChangeText(text: String?, cell: InboxCell) {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
// MARK: - Table view data source
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 Task.tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
cell = tableView.dequeueReusableCell(withIdentifier: "InboxCell", for: indexPath) as! InboxCell
// Configure the cell...
cell.delegate = self
cell.inboxTaskTextView.text = Task.tasks[indexPath.row]
return cell
}
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
tableView.beginUpdates()
Task.tasks.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
I have tried to scroll to the bottom of the table first and then making the newly created cell first responder but that didn't work. In that case only the very first cell created becomes the first responder while the subsequent cells are created but the cursor remains in the very first cell.
Here is the block of code I used for scrolling before cell.inboxTaskTextView.becomeFirstResponder() -
DispatchQueue.main.async {
self.tableView.reloadData()
let indexPath = IndexPath(row: Task.tasks.count - 1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
Edit -
After having tried for a while this is the closest I have got to a solution.
After Task.tasks.append("") I have added the following code which scrolls down the view to the bottom -
if tableView.contentSize.height >= tableView.frame.size.height {
DispatchQueue.main.async {
self.tableView.reloadData()
let indexPath = IndexPath(row: Task.tasks.count - 1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
In this case the newly created cell becomes first responder but only momentarily. The keyboard doesn't even appear fully before it gets dismissed automatically in a flash. This happens only for cells that are created below the fold - i.e. when the table view has to scroll down and then create a new cell.
Try to keep it simple first. Put a breakpoint or print("indexPath.row = \(indexPath.row)") at the beginning of cellForRow UITableViewDataSource method that you implemented already.
Add the new task and see if your cellForRow is being called for the indexPath corresponding to your new cell.
If not - you may have to scroll the tableView up at least 44points or whatever is needed to reach the area where the new cell should already be displayed. If you don't do that - the cell might not be created yet, and cell most probably refers to the last cell in the table view (or it could also be referring to a cell in the pool if some logic is not implemented correctly). So the new cell must be in the visible area before making its UITextField or UITextView become first responder.
If you know that cell is already visible - better to get an reference to it via
let index = IndexPath(row: rowForNewCell, section: 0)
let cell = self.table.cellForRow(at: index)
Finally call:
cell.inboxTaskTextView.becomeFirstResponder()
I have created a UITableView with multiple sections consisting two cells with dynamic cell height. Added a button on second cell. On click of the button increasing the second cell height by changing the constraint height. Action method connected to the button is :
func increaseHeightOnClickOfButton(sender: UIButton) {
let indexPath = IndexPath(item: 1, section: sender.tag)
let cell = myTableView.cellForRow(at: indexPath) as! customCell
cell.viewHeightConstraint.constant = 100
self.myTableView.reloadRows(at: [indexPath], with: .automatic)
}
Issue: On clicking the button, cell height of second section increases rather than the first one.
I have also tried giving fix value of section like this :
let indexPath = IndexPath(item: 1, section: 0)
but it's not working.
It works only when I try this code:
func increaseHeightOnClickOfButton(sender: UIButton) {
let indexPath = IndexPath(item: 1, section: sender.tag)
let cell = myTableView.cellForRow(at: indexPath) as! customCell
cell.viewHeightConstraint.constant = 100
self.myTableView.reloadData()
}
But I don't want to reload the whole table view. Can anyone help me what I am doing wrong or is there any other approach to achieve it ?
Edit :
I did one more change, I removed :
self.myTableView.reloadRows(at: [indexPath], with: .automatic)
After clicking the button when I start scrolling the tableview, coming back to the same cell increases it's height. So I guess there is some issue with reloadRows.
You can use the following code to reload cell's height without reloading the whole data:
tableView.beginUpdates()
tableView.endUpdates()
Edit:
Modified first post to include a DataSource to track each row's height constraint constant.
Assuming your cell looks something like this:
you have added a UIView to the cell (orange view in my example)
added a label and button to the UIView
set leading, trailing, top and bottom constraints for the view
added a Height constraint to the view
connected IBOutlets and an IBAction for the button tap inside the cell class
set your table class for UITableViewAutomaticDimension
Then this should be a working example. Each time you tap a button, the Height constraint in that cell will be increased by 20-pts. No need to call reload anything...
class TypicalCell: UITableViewCell {
#IBOutlet weak var theLabel: UILabel!
#IBOutlet weak var viewHeightConstraint: NSLayoutConstraint!
var btnAction: (() -> ())?
#IBAction func didTap(_ sender: Any) {
btnAction?()
}
}
class TypicalTableViewController: UITableViewController {
// 2-D array of Row Heights
// 4 sections, with 4, 2, 6 and 3 rows
// all initialized to 40.0
var rowHeights: [[CGFloat]] = [
[40.0, 40.0, 40.0, 40.0],
[40.0, 40.0],
[40.0, 40.0, 40.0, 40.0, 40.0, 40.0],
[40.0, 40.0, 40.0]
]
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 40.0
}
override func numberOfSections(in tableView: UITableView) -> Int {
return rowHeights.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rowHeights[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Typical", for: indexPath) as! TypicalCell
// Configure the cell...
cell.theLabel.text = "\(indexPath)"
// cells are reused, so set the cell's Height constraint every time
cell.viewHeightConstraint.constant = rowHeights[indexPath.section][indexPath.row]
// "call back" closure
cell.btnAction = {
// increment the value in our DataSource
self.rowHeights[indexPath.section][indexPath.row] += 20
// tell tableView it's being updated
tableView.beginUpdates()
// set the cell's Height constraint to the incremented value
cell.viewHeightConstraint.constant = self.rowHeights[indexPath.section][indexPath.row]
// tell tableView we're done updating - this will trigger an auto-layout pass
tableView.endUpdates()
}
return cell
}
}
Try reloading the Section:
self.tableView.reloadSections(IndexSet(integer: sender.tag), with: .automatic)
View Setup:
My TableView has 3 sections with 4 or 9 cell each. Each Cell has a Label and TextField.
On Starting to edit a cell at index 2 of each section, I reload the section which will now consist of 9 cells(update model to dequeueCell so that 5 more cells will be added).
Problem:
The tableView scrolls as expected(brings textfield to visible part of the screen) for the unexpanded state of the section. But after I add cells by beginning to edit the textfield of cell at index 2 of any section, the tableView scrolls such that it hides the textfield. The weird scrolling occurs for any cells in the tableview once any section has expanded numbers of cells. Also, while weird scroll is happening, the tableView is reloaded(which is leading to lose the focus away from textfield). I have included tableView.reloadSection(_:) in the didBeginEditing(:_) custom delegate of the cell.
I have seen this problem in iOS 9 and 10
Sorry for poor explanation. Thanks
Heres the Github Repo
And Problem is here
P.S. I am using Swift 3 and Xcode 8.3.3 with deployment target iOS 10
Please do not suggest answer in Swift 4 and Xcode 9
You can try another approach: change the height of cells instead of insert / delete.
Change number of cells to always return all items:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
guard let sectionEnum = Sections(rawValue: section) else { return 0 }
return sectionEnum.getRows(forExpanded: true).count
}
Set height of 'hidden' items to 0:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let sectionEnum = Sections(rawValue: indexPath.section) else { return 0 }
let isExpanded = expandedSectionData[indexPath.section]
if (!isExpanded) {
let object = sectionEnum.getRows(forExpanded: true)[indexPath.row]
if (!sectionEnum.getRows(forExpanded: false).contains(object)) {
return 0;
}
}
return self.tableView.estimatedRowHeight
}
Set cell to clip subviews to its bounds:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
....
cell.clipsToBounds = true;
return cell
}
And change updating code to (remove tableView.reloadSections, change indexPath):
func didBeginEditing(textField: UITextField, cell: UITableViewCell) {
guard let indexPath = tableView.indexPath(for: cell), let section = Sections(rawValue: indexPath.section) else { return }
if indexPath.row == 7 && !expandedSectionData[indexPath.section] {
expandedSectionData[indexPath.section] = true
tableView.beginUpdates()
tableView.endUpdates()
tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.none, animated: true)
textField.becomeFirstResponder()
}
}
You need to make textfield as first responder again, after reloading section text field no longer remains first responder.
You might need to change something like -
func didBeginEditing(textField: UITextField, cell: UITableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
if indexPath.row == 2 && !expandedSectionData[indexPath.section] {
tableView.beginUpdates()
expandedSectionData[indexPath.section] = true
tableView.reloadSections(IndexSet(integer: indexPath.section), with: .automatic)
tableView.endUpdates()
// after tableview is reloaded, get cell again
let cell = tableView.cellForRow(at: IndexPath(row: 2, section: indexPath.section)) as? TestCell
cell?.textField.becomeFirstResponder()
}
}
I have tried running this, kind of looks fine to me.
This issue has to do with your use of self-sizing tableview cells. To fix the issue, comment out these two lines in your viewDidLoad and consider defining the height of your cells with tableView:heightForRowAtIndexPath:.
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
Since the self-sizing tableview documentation states,
To define the cell’s height, you need an unbroken chain of constraints
and views (with defined heights) to fill the area between the content
view’s top edge and its bottom edge
I also tried changing the bottomMargin = textField.bottom constraint from priority 750 to 1000, but this did not fix the issue.
I have an UITableView with custom cells. Cells can be expanded when tapped.
I calculate the height with the delegate heightForRowAtIndexPath
The animation coming with reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None) looks really nice but doesn't work if a part of the bigger expanded cell is out of the screen. Then it just kind of pops (it looks like a jump), no nice "sliding"- animation (sorry i don't know the right animation-phrases), like when the cell is completely visible on the screen.
I already tried out reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) that made that nice sliding even if the expanded cell is out of the screen border but the upper part of my cell is just getting brighter for a moment and that doesn't look very fancy..
So anyone having trouble with that "animation" .None, too? ^^"
My did-select-func:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? ProviderNewTableViewCell {
if expandedCells.contains(indexPath) {
expandedCells.removeObject(indexPath)
}
else {
expandedCells.append(indexPath)
}
//This prevents the last cell glitching into the decreasing cell above
CATransaction.begin()
CATransaction.setCompletionBlock({ () -> Void in
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
})
CATransaction.commit()
}
}
My cell-for-row-func (the important parts ;) )
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("ProviderNewCell", forIndexPath: indexPath) as! ProviderNewTableViewCell
if expandedCells.contains(indexPath) {
indexPathDetailViewDict[indexPath] = ProviderCellBuilder.sharedInstance.buildDetailView(cell, prov: current)
}
}
I have added a feature to my app that allows users to add notes which will appear in table view cells when they are tapped as they expand to show more content.
My question is - How do I layout the content that is to be shown when the cell is expanded? I have tried adding the textView to the prototype cell, but I just get the textView overlapping on the other cells in the table view rather than being hidden before being tapped.
Here is my code for when a cell is tapped:
var selectedRowIndex: NSIndexPath = NSIndexPath(forRow: -1, inSection: 0)
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedRowIndex = indexPath
tableView.beginUpdates()
tableView.endUpdates()
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == selectedRowIndex.row {
if cellTapped == false {
cellTapped = true
return 141
} else {
cellTapped = false
return 70
}
}
return 70
}
You need to set the cell's clipToBounds property to YES (or select the "Clip Subviews" check box in IB for the cell), so none of its subviews will show outside the bounds of the cell. The text view should have constraints to the sides of the contentView as well as a height constraint. It should also have a constraint to something above it, and that view should have a constraint to the top of the contentView. The constraints should look something like this,