When you insert your first UITableViewCell with insertRowsAtIndexPaths:withRowAnimation:, it usually appears at the top of the UITableView. In the Periscope app, the opposite happens - the first inserted cell is bottom aligned. As new cells are pushed in, the old cells move up in the table. How is this achieved?
In case you're interested in how I did it in the Periscope iOS app, it's actually pretty simple...
TL;DR; Add a transparent table header header view with a height equal to your table view frame's height. Then, as you add cells to your table, simply animate the table view's content offset.
Give your table view a header view:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UIView *headerView = [[UIView alloc] initWithFrame:CGRectZero];
headerView.userInteractionEnabled = NO; // all touches within this space must go through to the video layer
return headerView; // empty header above chat, so messages flow in from bottom
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return self.tableView.frame.size.height; // empty header above chat, so messages flow in from bottom
}
Add data to your table (in my case, messages get added to an array called _messages. I then call reloadData on the UITableViewController). Then call this method to animate the cells in:
- (void)scrollTableToBottom
{
if (!self.isViewLoaded || _messages.count == 0)
return;
CGFloat offsetY = self.tableView.contentSize.height - self.tableView.frame.size.height + self.tableView.contentInset.bottom;
[UIView animateWithDuration:0.33
delay:0
options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction
animations:^{
[self.tableView setContentOffset:CGPointMake(0, offsetY) animated:NO];
}
completion:nil];
}
Hope that helps. I found this to be a pretty cheap/simple way of simulating cells anchored to the bottom. I know some people have mentioned flipping the table upside down, but that just seems crazy to me. :-)
Swift 4 version of Aaron Wasserman answer.
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: .zero)
headerView.isUserInteractionEnabled = false
return headerView
}
// Added logic to avoid blank space at the top
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return tableView.frame.size.height - CGFloat(messages.count * cellHeight)
}
func scrollToBottom(animated: Bool) {
if isViewLoaded && messages.count > 0 {
let offset = tableView.contentSize.height - tableView.frame.size.height + tableView.contentInset.bottom
UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut, .allowUserInteraction], animations: {
self.tableView.setContentOffset(CGPoint(x: 0, y: offset), animated: animated)
}, completion: nil)
}
}
scrollToBottom method needs to be called after tableView.reloadData()
My modification of the mr.Wasserman answer
extension MyViewController : UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let headerView = UIView(frame: .zero)
headerView.isUserInteractionEnabled = false
return headerView
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
let diff = tableView.contentSize.height - tableView.bounds.height
return diff > 0 ? 0 : -diff
}
}
You can use the insertRowsAtIndexPath method, and then get the tableview to scroll to bottom.
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:self.posts.count-1 inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
If you wish to have that giant gap at the top of your tableview you can set the scroll offset, and adjust this as new items come in.
Another way, would be to flip the tableview upside down, and then flip each row upside down.
Bit late to the party, but here's another approach.
It can be done without having to add arbitrary header views. Adjust the content offset whenever the content size changes.
class BottomEndianTableView: UITableView {
private var observer: Any?
override init(frame: CGRect, style: UITableView.Style) {
super.init(frame: frame, style: style)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
observer = observe(\.contentSize) { _, _ in
DispatchQueue.main.async { [weak self] in
self?.scrollToEnd(animated: false)
}
}
}
func scrollToEnd(animated: Bool) {
let scrollDistance = contentSize.height - frame.height
setContentOffset(CGPoint(x: 0, y: scrollDistance), animated: animated)
}
}
You can also call scrollToEnd(animated: true) in response to the keyboard displaying
Related
I have table view. Inside cell I have method that animates view inside cell.
func animateCell() {
UIView.animate(withDuration: 0.4, animations: { [weak self] in
self?.quantityBackround.transform = CGAffineTransform(scaleX: 25, y: 25)
}) { [weak self] _ in
UIView.animate(withDuration: 0.1) {
self?.quantityBackround.transform = CGAffineTransform.identity
}
}
}
Also I have prepareForReuse()
override func prepareForReuse() {
quantityBackround.transform = CGAffineTransform.identity
}
The animation must work only for last cell when array of datasource changes and I do this in property observer like this (fires when something is being added to array)
guard let cell = checkTableView.cellForRow(at: IndexPath(row: viewModel.checkManager.check.count - 1, section: 0)) as? CheckItemTableViewCell else { return }
cell.animateCell()
All of this works fine.
One problem, is that I encounter is that when tableView is reloaded, all background views in all cells expand from zero size to its initial. Last cell animates ok.
I think that i miss something in prepareForReuse and because of this i see this glitch of inreasing from zero to initial size.
How to fix it ?
You need to implement this method of UITableViewDelegate
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
//here check if your this cell is the last one, something like this
if (indexPath.row == yourDataSourceArray.count - 1)
{
if let customCell = cell as? CheckItemTableViewCell{
customCell.animateCell()
}
}
}
Hope this helps
I am trying to make a expandable table view (static cells). A container view is placed inside a cell below the "Lists of options" cell, as shown below:
The problem comes with the animation when expanding / collapsing. The topmost cell of each section will disappear briefly(hidden?), then reappear after the animation ends. I have later experimented and the results are as followed:
This will not happen when using tableView.reloadData().
This will happen when using tableView.beginUpdates() / endUpdates() pairs.
Hence I have come to the conclusion that this has sth to do with the animation. Below is my code and pictures for further explanation for said issue.
Code for hiding / showing
private func hideContainerView(targetView: UIView) {
if targetView == violationOptionsContainerView {
violationOptionContainerViewVisible = false
}
tableView.beginUpdates()
tableView.endUpdates()
UIView.animate(withDuration: 0.1, animations: { targetView.alpha = 0.0 }) { _ in
targetView.isHidden = true
}
}
private func showContainerView(targetView: UIView) {
if targetView == violationOptionsContainerView {
violationOptionContainerViewVisible = true
}
tableView.beginUpdates()
tableView.endUpdates()
targetView.alpha = 0.0
UIView.animate(withDuration: 0.1, animations: { targetView.alpha = 1.0 }) { _ in
targetView.isHidden = false
}
}
Table view
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
switch cell {
case violationTableViewCell:
if violationOptionContainerViewVisible {
hideContainerView(targetView: violationOptionsContainerView)
} else {
showContainerView(targetView: violationOptionsContainerView)
}
default: break
}
// tableView.reloadData()
// if we use this instead of begin/end updates, the cells won't disappear. But then we'll be ditching animation too.
}
tableView.deselectRow(at: indexPath, animated: true)
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 0 && indexPath.row == 3 {
if !violationOptionContainerViewVisible {
return 0.0
}
}
return super.tableView(tableView, heightForRowAt: indexPath)
}
Images
Note that the first cell in each section ("FFFF" and the "slider bar") disappears and reappears during the animation
After searching for a few days, I have found the cause to this behavior.
I have, for some reason, set the cells layer.zposition below default zero. This will cause the cell to be seen "below" it's background view (hence disappeared) during the animation even though it's a subview of it.
Adjusting the value back to 0 or higher will remove this issue.
I would like to set the UITableView to match the height for all the contents in the table view.
This is my storyboard
The problem with this is the top and bottom ImageView is always static on the screen.
The there are suppose to be 10 items on the table view but only 7 shows up due to screen size limitation. I would like to show all 10 before user is able to see the bottom ImageView. (btw, all 3 of the views ie. both the image views and tableview is in a uiscrollview)
IDEAL
Some of the other limitations that i have to work with is that the number of items in the table view is dynamic meaning it can be in any amount of usually less than 10 that i will later retrieve from an api. And the cell height is also dynamic depending on the contents.
I have only just started with some simple code
class ExampleViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var items: [String] = [
"Item 01", "Item 02", "Item 03", "Item 04", "Item 05",
"Item 06", "Item 07", "Item 08", "Item 09", "Item 10"]
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell")! as UITableViewCell
cell.textLabel?.text = self.items[indexPath.row]
return cell
}
}
Subclass your UITableView to override the intrinsicContentSize to be its contentSize, like this:
override var intrinsicContentSize: CGSize {
return contentSize
}
Then use automatic row heights for your table, so your exampleViewController's viewDidLoad would have:
tableView.estimatedRowHeight = 44
And the UITableViewDelegate function:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
When you receive data from your API and reload your table, just call:
tableView.invalidateIntrinsicContentSize()
This will tell your table to resize itself to the same size as its contents (because of the override), and move your bottom image as needed.
If your storyboard throws an error saying that your UIScrollView has an ambiguous height because there's no height constraint on the UITableView, select your UITableView and give it a placeholder intrinsic size in the Size Inspector.
The answers using the subclassing technique are incomplete. You should also override layoutSubviews() like this.
public class DynamicSizeTableView: UITableView
{
override public func layoutSubviews() {
super.layoutSubviews()
if bounds.size != intrinsicContentSize {
invalidateIntrinsicContentSize()
}
}
override public var intrinsicContentSize: CGSize {
return contentSize
}
}
This is what I utilize in production apps:
Swift 5, 2021
import UIKit
class DynamicTableView: UITableView {
/// Will assign automatic dimension to the rowHeight variable
/// Will asign the value of this variable to estimated row height.
var dynamicRowHeight: CGFloat = UITableView.automaticDimension {
didSet {
rowHeight = UITableView.automaticDimension
estimatedRowHeight = dynamicRowHeight
}
}
public override var intrinsicContentSize: CGSize { contentSize }
public override func layoutSubviews() {
super.layoutSubviews()
if !bounds.size.equalTo(intrinsicContentSize) {
invalidateIntrinsicContentSize()
}
}
}
You need to set an IBOutlet to the NSLayoutConstraint that sets the tableView height (first you need create the height constraint with any value, doesn't matter) and then ctrl drag it to your class file
Then in your viewWillAppear you have to calculate the tableView height and set it. Like this:
var tableViewHeight:CGFloat = 0;
for (var i = tableView(self.tableView , numberOfRowsInSection: 0) - 1; i>0; i-=1 ){
tableViewHeight = height + tableView(self.tableView, heightForRowAtIndexPath: NSIndexPath(forRow: i, inSection: 0) )
}
tableViewHeightLayout.constant = tableViewHeight
And that's pretty much it. That will give your scrollView content size and shouldn't raise any warnings.
Update Swift 4
this code working be good
self.scrollView.layoutIfNeeded()
self.view.layoutIfNeeded()
self.tableViewHeightConstraint.constant = CGFloat(self.tableView.contentSize.height)
You probably have to implement the table view intrinsic content size. Please check this answer to see if it helps.
I remember having this problem and even created a custom UITableView subclass.
#import "IntrinsicTableView.h"
#implementation IntrinsicTableView
#pragma mark - UIView
- (CGSize)intrinsicContentSize
{
return CGSizeMake(UIViewNoIntrinsicMetric, self.contentSize.height);
}
#pragma mark - UITableView
- (void)endUpdates
{
[super endUpdates];
[self invalidateIntrinsicContentSize];
}
- (void)reloadData
{
[super reloadData];
[self invalidateIntrinsicContentSize];
}
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super reloadSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super insertRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super insertSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
[super deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
[super deleteSections:sections withRowAnimation:animation];
[self invalidateIntrinsicContentSize];
}
#end
Update for Swift 5. Adding maxHeight so that you can specify how tall you want your tableView to be
class SelfSizingTableView: UITableView {
var maxHeight = CGFloat.infinity
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
setNeedsLayout()
}
}
override var intrinsicContentSize: CGSize {
let height = min(maxHeight, contentSize.height)
return CGSize(width: contentSize.width, height: height)
}
}
In that case, don't make your bottom cell static, make it a part of table view and insert this bottom image in last row using table view delegate method - insertRowAtIndexPath
In this type of case add your bottom imageView(red) in a table footer view.
To add footer view in UITableView you can use:
tableViewObj.tableFooterView = footerViewObj;
Try this also
in ViewDidLoad
self.table.estimatedRowHeight = 44.0 ;
self.table.rowHeight = UITableViewAutomaticDimension;
Height for row at index path
-(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return UITableViewAutomaticDimension;}
Easy way here.
Step 1: Set a height constraint for the table view
Step 2: Control drag the constraint
Step 3: Before you return the count of the rows. In numberOfRowsInSection method, do
tableViewHeightConstraint.constant = tableView.rowHeight * CGFloat(someArray.count)
Of course you can edit the height anchor programmatically, the logic here is to adjust the table view height according to the cell height and cell number.
Based on solution #nikans, written in Xamarin
[Register(nameof(DynamicSizeTableView)), DesignTimeVisible(true)]
public class DynamicSizeTableView : UITableView
{
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (Bounds.Size != IntrinsicContentSize)
InvalidateIntrinsicContentSize();
}
public override CGSize IntrinsicContentSize => ContentSize;
public DynamicSizeTableView(CGRect frame) : base(frame) { }
public DynamicSizeTableView(IntPtr handle) : base(handle) { }
}
Here is the simplest Solution
First Give a height to the tableView.
Create outlet of that height in view Controller. let's say tableViewHeight
Then do this in viewDidLoad or where you populate the data after calling tableView.reloadData()
var height = 0.0
for i in 0..<items.count {
let frame = tableView.rectForRow(at: IndexPath(row: i, section: 0))
height += frame.size.height
}
tableViewHeight.constant = height
This Also Works with tableViews that have dynamic cell heights
Based on solution of #rr1g0
Updated for Swift 5 in 2020, and works with TableViews with sections too.
Create height constraint for tableView and create an outlet to it. And in viewDidLayoutSubviews() use the code below:
var tableViewHeight: CGFloat = 0
for section in 0..<tableView.numberOfSections {
for row in 0..<tableView.numberOfRows(inSection: section) {
tableViewHeight += tableView(tableView, heightForRowAt: IndexPath(row: row, section: section))
}
}
tableViewHeightConstraint.constant = tableViewHeight
I am programmatically adding a UITableView as a subview of a view that uses UIView.animateWithDuration to expand the view when a button is clicked from a single point to a full window. Basically, a box that starts as a point and expands to full size with an animation. I am having difficulties getting the table to populate with cells. At first, a cell was being created, but would disappear after quickly after the animation completed, after playing around with it, I have gotten the cell to remain after the animation is complete, but now the cell disappears when I tap on it. I don't understand what is going on here. Can someone please help?
Here is my code. Note, I have removed what I believe to be irrelevant to this problem to make the code easier to read.
class PokerLogSelectionView: UIViewController {
let logSelectionTableViewController = LogSelectionTableViewController()
let logSelectionTableView = UITableView()
// Irrelevant class variables removed
init(btn : PokerLogSelectionButton){
// Irrelevant view initialization code removed
// Display the subviews
self.displayLogListScrollView()
}
func displayLogListScrollView() {
// Frame is set to (0,0,0,0)
let frame = CGRect(x: self.subviewClosed, y: self.subviewClosed, width: self.subviewClosed, height: self.subviewClosed)
logSelectionTableView.delegate = self.logSelectionTableViewController
logSelectionTableView.dataSource = self.logSelectionTableViewController
// Set the frame of the table view
logSelectionTableView.frame = frame
// Give it rounded edges
logSelectionTableView.layer.cornerRadius = 10
// Remove the cell divider lines
logSelectionTableView.separatorStyle = UITableViewCellSeparatorStyle.None
logSelectionTableView.backgroundColor = logSelectionViewContentScrollViewColor
self.view.addSubview(logSelectionTableView)
//self.logSelectionTableView.reloadData()
//self.addChildViewController(logSelectionTableViewController)
}
override func viewDidAppear(animated: Bool) {
// Create animation
let timeInterval : NSTimeInterval = 0.5
let delay : NSTimeInterval = 0
UIView.animateWithDuration(timeInterval, delay: delay, options: UIViewAnimationOptions.CurveEaseOut, animations: {
// Irrelevant code removed
// Set the size and position of the view and subviews after the animation is complete
self.view.frame = CGRect(x: self.frameXopen, y: self.frameYopen, width: self.frameWopen, height: self.frameHopen)
self.logSelectionTableView.frame = CGRect(x: self.subviewXopen, y: self.svYopen, width: self.subviewWopen, height: self.svHopen)
}, completion: { finished in
self.addChildViewController(self.logSelectionTableViewController)
})
}
}
class LogSelectionTableViewController : UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(LogSelectionCell.self, forCellReuseIdentifier: "logCell")
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pokerLibrary.logNames.count
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 20
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("Selected row: \(indexPath.row)")
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if let cell : LogSelectionCell = self.tableView.dequeueReusableCellWithIdentifier("logCell") as? LogSelectionCell {
cell.selectionStyle = UITableViewCellSelectionStyle.None
cell.textLabel!.text = pokerLibrary.logNames[indexPath.row].name
return cell
}
fatalError("Could not dequeue cell of type 'LogSelectionCell'")
}
}
Note: I can see the tableview after the animation is complete. The color is different than the view in the background view and the tableview does not disappear, just the cell. I expect there to be 1 cell, and I have printed out the number of rows in section 0 and it always returns 1.
Thanks for the help!
Edit:
Here is a screenshot of the view hierarchy before the cell disappears.
Here is a screenshot of the view hierarchy after I tap the cell and it disappears.
I overrode the touchesBegan method in my custom cell and did not call its superclass method. This stopped the cell from disappearing when I tap it, but it still disappears when I scroll the tableView.
I have a _TableView with items , and I want to set automatic refresh,and I don't want it to scroll on refresh , lets say user scrolled 2 pages down , and the refresh trigered -> so I want to put the refreshed content to the top of the table without interupting user's scrolling
Assume user was on row 18
and now the _dataSource is refreshed so it fetched lets say 4 items , so I want user to stay on the item he was.
What would be the best approach to achieve it ??
For Swift 3+:
You need to save the current offset of the UITableView, then reload and then set the offset back on the UITableView.
I have created this function for this purpose:
func reload(tableView: UITableView) {
let contentOffset = tableView.contentOffset
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(contentOffset, animated: false)
}
Simply call it with: reload(tableView: self.tableView)
SWIFT 3
let contentOffset = self.tableView.contentOffset
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
self.tableView.setContentOffset(contentOffset, animated: false)
This is error of iOS8 when using UITableViewAutomatic Dimension. We need store the content offset of table, reload table, force layout and set contenOffset back.
CGPoint contentOffset = self.tableView.contentOffset;
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
[self.tableView setContentOffset:contentOffset];
I am showing if only one row is being added. You can extend it to multiple rows.
// dataArray is your data Source object
[dataArray insertObject:name atIndex:0];
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y += [self tableView:self.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
[self.tableView reloadData];
[self.tableView setContentOffset:contentOffset];
But for this to work you need to have defined - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath the method. Or else, you can directly give your tableview row height if it is constant.
Just set estimatedRowHeight to maximum possible value.
self.tableView.estimatedRowHeight = 1000
self.tableView.estimatedSectionFooterHeight = 100.0
self.tableView.estimatedSectionHeaderHeight = 500.0
That's it!!
Note:
Please do not use FLT_MAX, DBL_MAX value. May be it will crash your app.
I'm doing it this way:
messages.insertContentsOf(incomingMsgs.reverse(), at: 0)
table.reloadData()
// This is for the first load, first 20 messages, scroll to bottom
if (messages.count <= 20) {
let indexToScroll = NSIndexPath(forRow: saferSelf.messages.count - 1, inSection: 0)
table.scrollToRowAtIndexPath(indexToScroll, atScrollPosition: .Top , animated: false)
}
// This is to reload older messages on top of tableview
else {
let indexToScroll = NSIndexPath(forRow: incomingMsgs.count, inSection: 0)
table.scrollToRowAtIndexPath(indexToScroll, atScrollPosition: .Top , animated: false)
// Remove the refreshControl.height + tableHeader.height from the offset so the content remain where it was before reload
let theRightOffset = CGPointMake(0, table.contentOffset.y - refreshControl.frame.height - table.headeView.frame.height)
table.setContentOffset(theRightOffset, animated: false)
}
...also, since I use dynamic cell height, to avoid some weirdness, the estimation is cached:
var heightAtIndexPath = [NSIndexPath: CGFloat]()
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return heightAtIndexPath[indexPath] ?? UITableViewAutomaticDimension
}
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
heightAtIndexPath[indexPath] = cell.frame.height
}
Use Extension
create UITableViewExtensions.swift and add following:
extension UITableView {
func reloadDataWithoutScroll() {
let offset = contentOffset
reloadData()
layoutIfNeeded()
setContentOffset(offset, animated: false)
}
}
In iOS 12.x, using Xcode 10.2.1, an easier option is.
UIView.performWithoutAnimation {
let loc = tableView.contentOffset
tableView.reloadRows(at: [indexPath], with: .none)
tableView.contentOffset = loc
}
This works better than following; it shakes at times when the row is not fully visible.
let contentOffset = self.tableView.contentOffset
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
self.tableView.setContentOffset(contentOffset, animated: false)
Swift 4.2 : Simple Solution
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 0
self.tableView.estimatedSectionHeaderHeight = 0
self.tableView.estimatedSectionFooterHeight = 0
}
//And then simply update(insert, reloadSections, delete etc) your tableView or reload
tableView.reloadData()
//or
UIView.performWithoutAnimation {
tableView.beginUpdates()
.....
tableView.endUpdates()
}
This code will prevent unnecessary animation and maintain the scroll view's content offset, it worked fine for me.
let lastScrollOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.reloadData()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastScrollOffset, animated: false)
When you want to reload you have to
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
and also use this UITableViewDelegate
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 'your maximum cell's height'
}
and your tableView will remain on the previous scroll position without scrolling
try to replace
reloadData with
tableView.reloadRows(at: tableView!.indexPathsForVisibleRows!, with: .none),
but you should be care about no cells, if no cells, this method should cause crash.
i wrote something that works perfect for me:
extension UIScrollView {
func reloadDataAndKeepContentOffsetInPlace(reloadData:(() -> Void)) {
let currentContentHeight = contentSize.height
if currentContentHeight == .zero {
reloadData()
return
}
reloadData()
layoutIfNeeded()
let newContentHeight = self.contentSize.height
DispatchQueue.main.async {
var contentOffset = self.contentOffset
contentOffset.y += newContentHeight - currentContentHeight
self.setContentOffset(contentOffset, animated: false)
}
}
}
use like this:
self.reloadSomeData()
collectionView.reloadDataAndKeepContentOffsetInPlace { [weak self] in
guard let self = self else { return }
self.collectionView.reloadData()
}
Try the following.
tableView.estimatedRowHeight = 0
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0
Source: https://developer.apple.com/forums/thread/86703