iOS Collection View shifts up when data reloads - ios

I have a TableView that manages two rows of data. Each row is managed by a CollectionView which allows the user to scroll horizontally and filter the data. The filters are part of the table view and above the CollectionView.
Here's a visual:
When I click the filters along row one (off screen in the image above) the ui smoothly updates the data. However when I click the filters along row two the collection view shifts up briefly before back down to the proper position.
I'm fairly confident that the issue has to do with my interface builder configuration but for the record, this is the code that reloads the collection views.
func onFetchCompleted() {
if shouldRefreshRow() {
//tableView.reloadData() // reload all rows
tableView.reloadRows(at: [IndexPath(row: viewModel!.rowToFetch!, section: 0)], with: .none)
}
}
func shouldRefreshRow() -> Bool {
return self.viewModel?.businessesStore[viewModel!.rowToFetch!].previousPage == 1
}
And here's the TableView config:
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? BusinessesTableViewCell {
cell.configureCell(dataSourceDelegate: self, filterDelegate: self, forPath: indexPath, indexPathsToReload:
return cell
}
return UITableViewCell()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if(viewModel?.businessesStore[collectionView.tag].businesses.count ?? 0 > 0) {
return (viewModel?.businessesStore[collectionView.tag].total)!
}
return 0
}
In interface builder I have my table view leading, trailing and bottom constraints set to 0 with respect to the superview and top to 0 with respect to the view above the table view (the solid black one in the image above).
I'm fairly certain this is an interface builder issue because If I remove my bottom constraint and set my TableView height to something like 400, the view behaves properly when a filter is clicked. The catch is that the user has to be scrolled down to the bottom of the screen otherwise it behaves in the janky manner explained above.
Here's the layout:
Any ideas?

I was able to find a solution to this issue after quite a bit of persistence. I had two things wrong in my configuration.
1: When I was reloading data with this line of code tableView.reloadRows(at: [IndexPath(row: viewModel!.rowToFetch!, section: 0)], with: .none) it seems to be including some sort of bounce animation on the collection view. I replaced that with tableView.reloadData() which fixed most of the bounce that was occurring.
2: I increased the size of my table view rows which removed the rest of the bounce.

Related

Inner UI Table View not scrolling

I have a nested tableview setup, where each parent tableViewCell contains a tableview of it's own. Each parent tableViewCell, expands or collapses accordingly when it's selected. However once the cell is expanded, the tableView of the cell is not scrollable. only parent tableView's scroll is performed.
Any help would be much appreciated!
/* sub 0, sub 1... table view is not scrolling */
The approach you are taking to implement the design is unnecessarily complicated and will create problems like this. The design you have can be very easily solved using one UITableView instance (without nesting another into all of it's UITableViewCell instances.
First of all, Delete your nested UITableView instance that's inside a UITableViewCell.
See following implementation -
import UIKit
class ViewController: UIViewController {
var expandedSections: Set<Int> = Set()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if expandedSections.contains(section) {
return 10 // 1 (parentRow) + number of subrows
} else {
return 1 // (parentRow)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
// return parentCell
} else {
// return subrowCell (with indexPath.row - 1 as index)
}
}
func expandSection(_ section: Int) {
tableView.beginUpdates()
if expandedSections.insert(section).inserted {
tableView.reloadSections(IndexSet(integer: section), with: .automatic)
}
tableView.endUpdates()
}
func collapseSection(_ section: Int) {
tableView.beginUpdates()
if expandedSections.remove(section) != nil {
tableView.reloadSections(IndexSet(integer: section), with: .automatic)
}
tableView.endUpdates()
}
}

UIPickerView inside static UITableViewCell animation on cell height update issue

I have a static UITableView in which I have a UIPickerView inside a UITableViewCell. When triggering didSelectRowAt it is supposed to toggle the height of the cell that has the UIPickerView. I use beginUpdates() and endUpdates() to change the height of the cell. It looks right when expanding but when collapsing, the UIPickerView doesn't animate and collapses faster than the cell does. Not sure what I am doing wrong.
I have made sure all views are set to true for clipToBounds on the UITableViewCell, ContentView (of the UITableViewCell) and UIPickerView. I have tried to wrap the beginUpdates/endUpdates in a DispatchQueue.main.async. My UIPickerView is using AutoLayout and (leading, trailing, top, bottom) edges are equal to Content View of the cell. I am using a StoryBoard for my UI.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch (indexPath.section, indexPath.row) {
case (1,1):
showPicker = !showPicker
tableView.beginUpdates()
tableView.endUpdates()
default:
()
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
switch (indexPath.section, indexPath.row) {
case (1, 1):
if !showDueDate {
return 0
}
case (1, 2):
if !showPicker {
return 0
}
case (_, _):
break
}
return super.tableView(tableView, heightForRowAt: indexPath)
}
I expected the animation to be smooth and UIPickerView to collapse alongside the UITabeViewCell.
see gif to see issue via GIPHY
Use self.tableView.reloadRows(at: IndexPath(row: indexPath.row, section: indexPath.section), with: .fade) instead of beginUpdates/endUpdates. It will give you a smooth animation when animating table changes.
Figured it out. My bottom constraint of the UIPickerView was set to the bottom of the content view, constant was 0. Changed it to <= 0 and it fixed it. Don't understand why 0 didn't work. Atleast I figured it out...

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..!!!

Unable to add consistent height and padding. between dynamic tableview cells

Im trying to implement a tableview that has cells with a product image on the left side and a listing of various product details such as price, title, etc. to the right of the image, however, I have been having trouble with setting up the appropriate spacing/padding between the cells. I want each cell to have some white space between them. Here is an image showing what i have so far, and what the issue is.
TableView
I am using a stackview as a container for the labels with the product details, and am implementing the dynamic row height feature:
self.tableView.rowHeight = UITableViewAutomaticDimension
Everything seems to work great except wajjhen the stackview height is smaller than the imageview height. I can't get the spacing to show in that scenario. I have tried everything for the past 2 days, and it is driving me crazy. Take it easy on me guys as I am an Android guy :)
All I want is for the I want the separators of tableview cells to have a bit of space between either the top and bottom of the imageview or the stackview(whichever is taller/higher).
I have attached a SS of the storyboard to show the layouts and constraints. Please let me know if any more information is need.
Storyboard
for adding consitent spacing to your cell better way is to treat your each item as a section because you can set spacing easily between sections by creating a header view between section.
So logically if you have 10 objects than you will be having 10 section with each section have only one row.
1.Each section with only one row
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
2.Number of section is equal to count array of items
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return array.count
}
3.Set Hieght for spacing/header view
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 10
}
4.create your header view and customise it
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if let view = view as? UITableViewHeaderFooterView{
view.backgroundView?.backgroundColor = UIColor.clearColor()
}
}
Assuming that you have no problem with setting the appropriate height for each cell (if you have, you might want to check this Q&A), I suggest to let each cell on a section instead adding them in the same one.
Technique:
By default, if you are not implementing number​Of​Sections(in:​), it returns 1. You should:
implement it and let it returns the number of array data source -for example- (instead of doing it in table​View(_:​number​Of​Rows​In​Section:​)).
Let table​View(_:​number​Of​Rows​In​Section:​) returns 1. Each section will has only one cell.
After that, the trick is to add a header view for the sections (with a clear background color) with desired height, the height will be the margin between the section (cells in your case). You should achieve this by implementing:
table​View(:​view​For​Header​In​Section:​) and table​View(:​height​For​Header​In​Section:​).
The code will be similar to:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
// ...
// MARK:- UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell-ID")
// note that you should read from 'indexPath.section' instead of 'indexPath.row'
let currentObejct = dataSource[indexPath.section]
}
// MARK:- UITableViewDelegate
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// here, you can customize the header
// or your case want to let it clear...
let margin = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.size.width, height: 20.0))
margin.backgroundColor = UIColor.clear
return margin
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 20.0
}
// ...
}
Hope this helped.
You can try giving the Fruit Image a top and a bottom constraint but they must be >= so it doesn't mess your auto layout
And your constraints will probably look like this
3 days later, I finally found a solution I am content with, albeit it is quite janky, but at this point(3 days of dealing with this issue), I don;t really care. I am posting my solutuion in case it helps anyone in the future.
I ended up pinning a view to the tableviewcell with a minimum height of 130 to contain the imageview and stackview. Since I know the minimum height of the tableviewcells is 120, 130 gave a padding of 5 on the top and bottom. I attached the screenshot of my storyboard:
Storyboard
TableView

Resources