How to fix voiceover after deleting a row in a TableView - ios

I've got some problems fixing voiceover after deleting a row. the structure is like this:
I've got a tableview with 2 sections.
The first section got a header of height = 0 and only one row of variable height.
The second section got a header with fixed height with a button inside; rows in this sections can be 'n'.
When the user tap the button inside the header the cell in the first section is deleted or re-inserted according to the previous state.
In the normal 'state' with the cell expanded the voiceover works perfectly. When the user taps the button and delete the row in the first section the voiceover breaks. If I browse from top to bottom it's all ok. Instead when you scroll upwards the vo reads the cells visible on the screen but reads the header of the first section before the cells below it.
Insert and deleting is pretty simple:
let indexPath = IndexPath(row: 0, section: 0)
if isExpanded {
if tableView.contentOffset.y <= 0 {
tableView.insertRows(at: [indexPath], with: .automatic)
} else {
tableView.reloadData()
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
} else {
tableView.deleteRows(at: [indexPath], with: .automatic)
}
cells in each sections have: isAccessibilityElement = false
the accessibility element is the card inside the cell like this:
cardView.isAccessibilityElement = true
cardView.accessibilityTraits = .button
I would really appreciate your help, I have tried different solutions but none of them work. It's pretty much a headache!
Let me know if you need more info to solve this.
Thank you.

Solved this by keeping always a row in the first section.
tableView.performBatchUpdates({
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.insertRows(at: [indexPath], with: .fade)
}, completion: nil)
But when the variable isExpanded is false the height of the rows in first section is set to 0 instead of automatic.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 0 {
return isExpanded ? UITableView.automaticDimension : 0
}
return UITableView.automaticDimension
}

Related

UITableView Scrolls to top after insert row

I have an expandable UITableView. When sections tapped, they expand or collapse with animation (Scroll). My problem is that there is a weird animation when expanding or collapsing headers. UITableView scrolls to top and then goes to the tapped cell. In addition, when there is no expanded cell, sometimes, It doesn't scroll to top and there is a big space between top header and top view of UITableView.
My problem is that I need to scroll to expanded section and also get rid of scroll to top bug.
I tried below solution but didn't work for me:
prevent table view to scrolling top after insertRows
It also looks like same problem with below question, but can't figure out how to implement it.
Why does my UITableView "jump" when inserting or removing a row?
How I toggle selection:
func toggleSection(header: DistrictTableViewHeader, section: Int) {
print("Trying to expand and close section...")
// Close the section first by deleting the rows
var indexPaths = [IndexPath]()
for row in self.cities[section].districts.indices {
print(0, row)
let indexPath = IndexPath(row: row, section: section)
indexPaths.append(indexPath)
}
let isExpanded = self.cities[section].isExpanded
if(isExpanded){
AnalyticsManager.instance.logPageEvent(screenName: analyticsName!, category: "Button", action: Actions.interaction, label: "\(self.cities[section].name) Collapse Click")
}else{
AnalyticsManager.instance.logPageEvent(screenName: analyticsName!, category: "Button", action: Actions.interaction, label: "\(self.cities[section].name) Expand Click")
}
self.cities[section].isExpanded = !isExpanded
// This call opens CATransaction context
CATransaction.begin()
// This call begins tableView updates (not really needed if you only make one insertion call, or one deletion call, but in this example we do both)
tableView.beginUpdates()
if isExpanded {
tableView.deleteRows(at: indexPaths, with: .automatic)
} else {
tableView.insertRows(at: indexPaths, with: .automatic)
}
// completionBlock will be called after rows insertion/deletion animation is done
CATransaction.setCompletionBlock({
// This call will scroll tableView to the top of the 'section' ('section' should have value of the folded/unfolded section's index)
if !isExpanded{
self.tableView.scrollToRow(at: IndexPath(row: NSNotFound, section: section) /* you can pass NSNotFound to scroll to the top of the section even if that section has 0 rows */, at: .top, animated: true)
}
})
if self.scrollToTop(){
self.tableView.setContentOffset(.zero, animated: true)
}
// End table view updates
tableView.endUpdates()
// Close CATransaction context
CATransaction.commit()
}
private func scrollToTop() -> Bool{
for sec in self.cities{
if(sec.isExpanded){
return false
}
}
return true
}
I'm giving height of cell inside;
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}
How I declare headers;
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = DistrictTableViewHeader()
header.isColapsed = !self.cities[section].isExpanded
header.customInit(title: self.cities[section].name, section: section, delegate: self)
return header
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 60
}
EDIT: Solution in this question (Setting estimated height to 0) looks like working when inserting row. However, I still have bug when deleting rows. Bottom header goes to center of tableview and then goes to bottom after collapse header.
iOS 11 Floating TableView Header
You can try using below code. Just get the last content offset of your tableview. Then do the update and reassign the content offset.
let lastOffset = tableView.contentOffset
self.tableView.beginUpdates()
self.tableView.layer.removeAllAnimations()
self.tableView.endUpdates()
tableView.contentOffset = lastOffset
Instead of tableView.beginUpdates() and tableView.endUpdates(), In my Code i'm using tableView.reloadData() for expanding and contracting the particular section, You can call reloadData when you need to provide expansion of section.This results that you don't have the problem of animation scroll to the top. And works fine in my project where I have to show number of rows in particular section on a click of button which includes in that section.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 1
}
// Ignore the If Else statements it's just when do you need expansion of section.
else {
if showMore == true {
return self.userPoints.rewardsData[section - 1].count - 1
}
else {
return 0
}
}
}
Also Don't Forget to increase or decrease the number of rows to that particular section accordingly.Previous line is important to avoid any crash.
Simple Solution swift3 and Above
use below extension as
eg: tableViewOutlet.tableViewScrollToBottom(animated: true)
extension UITableView {
func tableViewScrollToBottom(animated: Bool) {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
let numberOfSections = self.numberOfSections
let numberOfRows = self.numberOfRows(inSection: numberOfSections-1)
if numberOfRows > 0 {
let indexPath = IndexPath(row: 0, section: 0)
self.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.top, animated: animated)
}
}
}
}
I also facing same issue but after read some tutorials and research & Analysis I got the this issue occurred due to height of cell when you expand the section at that tableview count height of cell from 0 to 120(as per your cell height).
In my case I solved that issue using estimated height of cell.
I hope that will help you,
Thanks

UITableView is jumping when I insert new rows

I tried many ways to solve this problem, but I couldn't. My tableView jumps after it loads more data. I call the downloading method in willDisplay:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastObject = objects.count - 1
if indexPath.row == lastObject {
page = page + 1
getObjects(page: page)
}
}
and insert rows here:
func getObjects(page: Int) {
RequestManager.sharedInstance.getObjects(page: page, success: { objects in
DispatchQueue.main.async(execute: {
self.objects = self.objects + objects
self.tableView.beginUpdates()
var indexPaths = [IndexPath]()
for i in 0...objects.count - 1 {
indexPaths.append(IndexPath(row: i, section: 0))
}
self.tableView.insertRows(at: indexPaths, with: .bottom)
self.tableView.endUpdates()
});
})
}
So what do I wrong? Why tableView jumps after inserting new rows?
I have just find the solution to stop jumping the table view while
inserting multiple rows in the table View. Am facing this issue from
last few days so finally I resolved the issue.
We have to just set the content offset of table view while
inserting rows in the table view. You have to just pass your array of
IndexPath rest you can use this function.
Hope so this method will help you.
func insertRows() {
if #available(iOS 11.0, *) {
self.tableView.performBatchUpdates({
self.tableView.setContentOffset(self.tableView.contentOffset, animated: false)
self.tableView.insertRows(at: [IndexPath], with: .bottom)
}, completion: nil)
} else {
// Fallback on earlier versions
self.tableView.beginUpdates()
self.tableView.setContentOffset(self.tableView.contentOffset, animated: false)
self.tableView.insertRows(at: [IndexPath], with: .right)
self.tableView.endUpdates()
}
}
I had a similar problem with tableView. Partially I decided this with beginUpdates() and endUpdates()
self.tableView.beginUpdates()
self.tableView.endUpdates()
But this didn't solve the problem.
For iOS 11, the problem remained.
I added an array with the heights of all the cells and used this data in the method tableView(_:heightForRowAt:)
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row] ?? 0
}
Also add this method tableView(_:estimatedHeightForRowAt:)
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row] ?? 0
}
After that, the jumps stopped.
First, check your tableView(_:estimatedHeightForRowAt:) - this will never be accurate but the more likely the cell height ends up with this estimate the less work the table view will do.
So if there are 100 cells in your table view, 50 of them you are sure will end up with a height of 75 - that should be the estimate.
Also it's worth a while noting that there is no limit on the number of times the table view may ask its delegate of the exact cell height. So if you have a table view of 1000 rows there will a big performance issue on the layout out of the cells (delays in seconds) - implementing the estimate reduces drastically these calls.
Second thing you need to revisit the cell design, are there any views or controls whose height need to calculated by the table view? Like an image with top and bottom anchors equivalent to some other view whose height changes from cell to cell?
The more fixed heights these views/ controls have the easier it becomes for the table view to layout its cells.
I had the same issue with two table views, one of them had a variable height image embedded into a stack view where I had to implement the estimate. The other didn't had fixed size images and I didn't need to implement the estimate to make it scroll smoothly.
Both table views use pagination.
Last but not least, arrays are structs. structs are value types. So maybe you don't want to store any heights in an array, see how many copies you're making?
calculating the heights inside tableView(_:heightForRowAt:) is quite fast and efficient enough to work out really well.
Because your loop runs from 0 to objects count:
for i in 0...objects.count - 1 {
indexPaths.append(IndexPath(row: i, section: 0))
}
The indexpaths generated counting for row 0 till object's count. and hence the rows are getting added at top of table (i.e. at row 0) and hence causing tableview to jump as you are there at bottom of tableview.
Try changing range as:
let rangeStart = self.objects.count
let rangeEnd = rangeStart + (objects.count - 1)
for i in rangeStart...rangeEnd {
indexPaths.append(IndexPath(row: i, section: 0))
}
Hope it helps..!!!

Tableview is bouncing when reloadRows(at: [indexPath], with: .fade) is called from did select row at index path

I am having this weird issue. when I scroll tableview and update some cell it bounce equivalent to the amount first cell scrolled.
I am having one view controller with tableview in it and based on user select some rows i'm updating only that row using below code
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let index = indexPath.row
print(index)
let service = services[indexPath.row]
if service.isEnabled {
service.isEnabled = false
}
else {
service.isEnabled = true
}
let indexPath = IndexPath(item: index, section: 0)
serviceTableView.reloadRows(at: [indexPath], with: .fade)
}
this is working fine if I don't scroll the table view but if i scroll table view and select some cell its bounce the entire tableview
Things already tried
Removing all constraints of table view, it still bounce
Removing all config methods from cellForRowAt indexPath
Different simulators
also I am not doing anything in section view.
PS: What I observe is that it only happens when first cell is partially visible/hidden.

TableView Auto Scrolling misbehaving after adding cells

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.

iOS 11 UITableViewCell above viewForHeaderInSection view after deleting

I have simple UITableViewController.
View for header. Which inits from xib.
And single type of cell.
After deleting cell with swipe, cell which above deleted one become visible above HeaderView, when other cells just hides below HeaderView as it should be.
If something above not clear - ask.
Video: https://youtu.be/aX-iPnM3q4Q
I think this should solve it:
tableView.sendSubview(toFront: tableView.headerView(forSection: 0)!)
(assuming you have only 1 section)
You can set this after deleting the cell, for example in commitEditingStyle.
Found the solution.
func deleteItem(from indexPath: IndexPath, tableView: UITableView) {
cellsData.remove(at: indexPath.row)
CATransaction.begin()
tableView.beginUpdates()
CATransaction.setCompletionBlock {
tableView.reloadData()
}
tableView.deleteRows(at: [indexPath], with: .left)
tableView.endUpdates()
CATransaction.commit()
}
tableView.reloadData() do the thing that fixing appearing cell above the headerView, but kills animation. So if you don't care about animation you can add only reloadData().

Resources