Whenever I call
reloadRowsAtIndexPaths
My UITableView contentOffset is removed, is there a delegate method I can use to catch the table view updating and set the Offset again so that it remains in place and does not animate into view, or simply prevent it doing this?
I am setting the contentOffest in viewDidLoad:
self.tableView.contentOffset = CGPointMake(0, 43);
Here is an example usage:
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[params valueForKey:#"index"], nil] withRowAnimation:UITableViewRowAnimationFade];
Which removes the contentOffset and animates it into view, which I do not want.
More specifically this appears to occur when the row being reloaded is at indexPath.section 0 and indexPath.row 0, i.e. the top row.
More Information
I am calling reloadRowsAtIndexPaths after an asynchronous request to fetch an image from a server. It basically works like so:
cellForRowAtIndexPath is called which checks for the presence of the thumb file on the disk, if the file is not present a placeholder is loaded in it's place and an asynchronous request is started in a background thread to fetch the image.
When the image download has completed I call reloadRowsAtIndexPaths for the correct cell so that the correct image fades in in place of the placeholder image.
The amount of cells may be different as the request is called inside cellForRowAtIndexPath so that the images load in as the cells load
cellForRowAtIndexPath file check
paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
path = [[paths objectAtIndex:0] stringByAppendingPathComponent:[NSString stringWithFormat:#"%#_small.gif",[[listItems objectAtIndex:indexPath.row] valueForKey:#"slug"]]];
if([[NSFileManager defaultManager] fileExistsAtPath:path]){
listCell.imageView.image = [UIImage imageWithData:[NSData dataWithContentsOfFile:path]];
} else {
listCell.imageView.image = [UIImage imageNamed:#"small_placeholder.gif"];
NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithObjectsAndKeys:[[[listItems objectAtIndex:indexPath.row] valueForKey:#"image"] valueForKey:#"small"],#"image",[NSString stringWithFormat:#"%#_small.gif",[[listItems objectAtIndex:indexPath.row] valueForKey:#"slug"]], #"name",indexPath,#"index",#"listFileComplete",#"notification",nil];
[NSThread detachNewThreadSelector:#selector(loadImages:) toTarget:self withObject:params];
[params release];
}
File donwloaded notification:
-(void)fileComplete:(NSNotification *)notification {
[self performSelectorOnMainThread:#selector(reloadMainThread:) withObject:[notification userInfo] waitUntilDone:NO];
}
Cell reload ( I have hardcoded sections due to a bug with strange section numbers being passed very rarely causing a crash:
-(void)reloadMainThread:(NSDictionary *)params {
NSIndexPath *index;
switch ([[params valueForKey:#"index"] section]) {
case 0:
index = [NSIndexPath indexPathForRow:[[params valueForKey:#"index"] row] inSection:0];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[params valueForKey:#"index"], nil] withRowAnimation:UITableViewRowAnimationNone];
break;
default:
index = [NSIndexPath indexPathForRow:[[params valueForKey:#"index"] row] inSection:2];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:index,nil] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
EDIT: My original answer may not have focused on the core problem
Are you changing your number of rows before the call to reloadRows...? reloadRows... is specifically to animate a value change, so your code should look something like this:
UICell *cell = [self cellForRowAtIndexPath:indexPath];
cell.label.text = #"Something new";
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]];
Is that more or less what you look like, but the tableview is forgetting where it is?
Previous discussion
You don't call -beginUpdates and -endUpdates around a reload. You call -beginUpdates and -endUpdates around your related modifications of the backing data, during which you should be calling -insertRowsAtIndexPaths:withRowAnimation: and its relatives. If you call the updating routines, then you don't need to call the reload routines. Reloading in the middle of an update is undefined behavior.
See Batch Insertion, Deletion, and Reloading of Rows and Sections for details on when you use -beginUpdates.
Sounds like you are running into this problem due to incorrectly estimated row heights. Because (for some mysterious reason) the table view determines the new offset after reloading some cells using the estimated row height you want to make sure the tableView:estimatedHeightForRowAtIndexPath returns correct data for cells that have already been rendered. To accomplish this you could cache the seen row heights in a dictionary:
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
heightForIndexPath[indexPath] = cell.frame.height
}
then use this correct data or your estimate for not already loaded cells:
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return heightForIndexPath[indexPath] ?? averageRowHeight
}
(HUGE thanks to eyuelt for the insight that estimated row height is used to determine the new offset.)
Have you tried the following:
CGPoint offset = [self.tableView contentOffset];
CGSize size = [self.tableView contentSize];
CGFloat percentScrolled = offset.y / size.height;
// call reloadRowsAtIndexPaths, inserts, deletes etc.
// ...
CGSize newSize = [self.tableView contentSize];
CGSize newOffset = CGPointMake(0, newSize.height * percentScrolled);
[self.tableView setContentOffset:newOffset animated:NO];
This seems a little too trivial, not sure it would work, but worth a shot.
Replace
[_tableView reloadData];
with
[_tableView reloadRowsAtIndexPaths:...withRowAnimation:...];
and it will be good.
I use this:
-(void)keepTableviewContentOffSetCell:(cellClass *)cell{
CGRect rect = cell.frame;
self.tableView.estimatedRowHeight = rect.size.height ;
}
You can use it before you reload the cell. The table view won't scroll when reload cell.
Related
How to get exact starting position of a first cell in UITableview?
First you need to get the rect in your table
CGRect cellRectInTable = [tableView rectForRowAtIndexPath:indexPath];
Then you get convert that to super view
CGRect cellInSuperview = [tableView convertRect:cellRectInTable toView:[tableView superview]];
You may also have luck with
CGRect cellInWindow = [tableView convertRect:cellRectInTable toView:nil];
Then you can access the origins of those rects
...rect.origin.x
...rect.origin.y
Apple Docs UIView convRect
Apple Docs rectForRowAtIndexPath
You can get it by using:
int y = cell.frame.origin.y;
int x = cell.frame.origin.x;
Try this:
int numberOfCells = [self.tableView numberOfRowsInSection:0]; // whatever section number
if (numberOfCells > 0) {
NSIndexPath *firstCellIndex = [NSIndexPath indexPathForRow:0 inSection:0];
UITableViewCell *firstCell = [self.tableView cellForRowAtIndexPath:firstCellIndexPath];
NSLog(#"%d, %d", firstCell.frame.origin.x, firstCell.frame.origin.y);
}
Call the following after you call dequeueReusableCellWithIdentifier:forIndexPath: in the delegate method tableView:cellForRowAtIndexPath: then store it in a CGPoint property.
To compact it more
if (self.indexPath.row == 0 && self.indexPath.section == 0){
CGPoint point = CGPointMake(cell.frame.origin.x, cell.frame.origin.y);
self.firstPoint = point;
}
Of course, you could just put self.firstPoint where point is, but I left them separate to demonstrate.
You can also get the cell from the index path in any given method by calling
[self.tableView cellForRowAtIndexPath: indexPath]
I have a UITableViewCell with UISwitch as accessoryview of each cell. When I change the value of the switch in a cell, how can I know in which row the switch is? I need the row number in the switch value changed event.
Tags, subclasses, or view hierarchy navigation are too much work!. Do this in your action method:
CGPoint hitPoint = [sender convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *hitIndex = [self.tableView indexPathForRowAtPoint:hitPoint];
Works with any type of view, multi section tables, whatever you can throw at it - as long as the origin of your sender is within the cell's frame (thanks rob!), which will usually be the case.
And here it is in a UITableView Swift extension:
extension UITableView {
func indexPath(for view: UIView) -> IndexPath? {
let location = view.convert(CGPoint.zero, to: self)
return self.indexPathForRow(at: location)
}
}
If you set the tag property to the row number (as suggested by other answers), you have to update it every time in tableView:cellForRowAtIndexPath: (because a cell can be reused for different rows).
Instead, when you need the row number, you can walk up the superview chain from the UISwitch (or any other view) to the UITableViewCell, and then to the UITableView, and ask the table view for the index path of the cell:
static NSIndexPath *indexPathForView(UIView *view) {
while (view && ![view isKindOfClass:[UITableViewCell class]])
view = view.superview;
if (!view)
return nil;
UITableViewCell *cell = (UITableViewCell *)view;
while (view && ![view isKindOfClass:[UITableView class]])
view = view.superview;
if (!view)
return nil;
UITableView *tableView = (UITableView *)view;
return [tableView indexPathForCell:cell];
}
This doesn't require anything in tableView:cellForRowAtIndexPath:.
in cellForRowAtIndexPath:, set the tag property of your control to indexPath.row
Accepted solution is a clever hack.
However why do we need to use hitpoint if we can utilize already available tag property on UIView? You would say that tag can store only either row or section since its a single Int.
Well... Don't forget your roots guys (CS101).
A single Int can store two twice-smaller size integers.
And here is an extension for this:
extension Int {
public init(indexPath: IndexPath) {
var marshalledInt: UInt32 = 0xffffffff
let rowPiece = UInt16(indexPath.row)
let sectionPiece = UInt16(indexPath.section)
marshalledInt = marshalledInt & (UInt32(rowPiece) << 16)
marshalledInt = marshalledInt + UInt32(sectionPiece)
self.init(bitPattern: UInt(marshalledInt))
}
var indexPathRepresentation: IndexPath {
let section = self & 0x0000ffff
let pattern: UInt32 = 0xffff0000
let row = (UInt32(self) & pattern) >> 16
return IndexPath(row: Int(row), section: Int(section))
}
}
In your tableView(_:, cellForRowAt:) you can then:
cell.yourSwitch.tag = Int(indexPath: indexPath)
And then in the action handler you would can:
func didToogle(sender: UISwitch){
print(sender.tag.indexPathRepresentation)
}
However please note it's limitation: row and section need to be not larger then 65535. (UInt16.max)
I doubt your tableView's indexes will go that high but in case they do, challenge yourself and implement more efficient packing scheme. Say if we have a section very small, we don't need all 16 bits to represent a section. We can have our int layout like:
{section area length}{all remaining}[4 BITS: section area length - 1]
that is our 4 LSBs indicate the length of section area - 1, given that we allocate at least 1 bit for a section. Thus in case of our section is 0, the row can occupy up to 27 bits ([1][27][4]), which definitely should be enough.
I prefer using subviews, if you know your layout it's generally super simple and 1 line short...
NSIndexPath *indexPath = [tableView indexPathForCell:(UITableViewCell *)[[sender superview] superview]];
Thats it, if its more nested, add in more superviews.
Bit more info:
all you are doing is asking for the parent view and its parent view which is the cell. Then you are asking your tableview for the indexpath of that cell you just got.
One common way to do this is to set the tag of the control (in your case the switch) to something that can be used to identify the row or represented object.
For example, in tableView:cellForRowAtIndexPath: set the tag property of the switch to the indexPath.row and in your action method you can get the tag from the sender.
Personally, I don't like this approach and prefer subclassing UITableViewCell.
Also, it may be a good idea to add an "offset" to the tag to prevent any conflicts with the tags of other views.
The accepted answer on this post is perfectly fine. I'd like to suggest to readers that the following, derived from #robmayoff on this post, is also perfectly fine:
- (NSIndexPath *)indexPathForView:(UIView *)view inTableView:(UITableView *)tableView {
while (view && ![view isKindOfClass:[UITableViewCell class]])
view = view.superview;
UITableViewCell *cell = (UITableViewCell *)view;
return [tableView indexPathForCell:cell];
}
Some have asserted that this approach contains too much computational work because of the while loop. The alternative, convert the view's origin to table view coordinate space and call indexPathForRowAtPoint:, hides even more work.
Some have asserted that this approach is unsafe relative to potential SDK changes. In fact, Apple has already changed the tableview cell hierarchy once, adding a contentView to the cell. This approach works before and after such a change. As long as view ancestors can be found via a chain of superviews (which is as fundamental as anything in UIKit), this is good code.
A colleague suggested the following, which I made into a UITableView category:
+(UITableViewCell*)findParentCellForSubview:(UIView*)view
{
while (([view isKindOfClass:[UITableViewCell class]] == NO) && ([view superview] != nil))
view = [view superview];
if ([view superview] != nil)
return (UITableViewCell*)view;
return nil;
}
Still hackly - but it works.
One more variant of using superView. Works like category for UIView.
- (UITableViewCell *)superCell
{
if (!self.superview) {
return nil;
}
if ([self.superview isKindOfClass:[UITableViewCell class]]) {
return (UITableViewCell *)self.superview;
}
return [self.superview superCell];
}
i dont know about the multiple sections but i can give you for the one section...
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger index=indexPath.row;
NSString *string=[[NSString alloc]initWithFormat:#"%ld",(long)index];
}
from this you can get the row number and you can save it to the string....
I have a tableVIew, where each cell is 90.0f tall (the height). my code;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 90.0;
}
Now when i delete all the records in the table. the height of the cell shrinks to its default value which is 44 (i think).
1.) My problem is that, i need the height of the cell to remain as 90.0f even after the user deletes all the records. How can i do this programatically ?
2.) I have given 2 colours to my cell.
cell.contentView.backgroundColor = indexPath.row % 2 ? [UIColor whiteColor] : [UIColor blackColor] ;
When i delete the second cell, which is black, then both the 1st and 3rd cell is white. How can i re-arrange this to be black and white in an order.
EDIT:
Ok this works but when i try to delete the last record in the table and [tableview indexPathsForVisibleRows] i end up getting Assertion failure in
-[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1448.89/UITableView.m:961 and
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. Attempt to delete more rows than exist in section.'
Why is this ?
CODE:
[self.tableView beginUpdates];
[self.myArray removeObjectsInArray:discardedItems ];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:YES];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:YES];
[self.tableView endUpdates];
1) I don't think you can do that. One way around it would be to show x amount of blank cells to fill the page.
2) Call [tableView reloadData] upon cell deletion, or call [tableView reloadCellAtIndex...] on the rows before and after the deleted cell.
If the height of your rows is really static (like you showed), consider not implementing tableView:heightForRowAtIndexPath: but use the table-view's rowHeight property, instead.
This can be set either in code or in Interface Builder and should be the solution to your "the rows are shrinking if there are none" problem.
To your second problem:
Do you know about -[UITableView indexPathsForVisibleRows]?
You can use this to only reload the part of your table-view that needs to be updated. It would go like this:
- tableView:(UITableView *)table commitEditingStyle:(UITableViewCellEditingStyle)style forRowAtIndexPath:(NSIndexPath *)path
{
NSArray *visibleCellPaths = [table indexPathsForVisibleRows];
// replace this comment with whatever else needs to be done in any case...
if (style == UITableViewCellEditingStyleDelete) {
// replace this comment with the deletions of the model object
NSUInteger affectedPathIndex = [visibleCellPaths indexOfObject:path];
NSMakeRange updateRange = NSMakeRange(affectedPathIndex, [visibleCellPaths count] - affectedPathIndex);
NSArray *cellPathsToReload = [visibleCellPaths subarrayWithRange:updateRange];
// replace this comment with the bounds-checking.
// e.g. if you only had one section and the backing store was an array, it may look like this:
// while ([self.backingStore count] <= [[cellPathsToReload lastObject] row]) {
// NSUInteger remainingPathCount = [cellPathsToReload count];
// if (remainingPathCount) break;
// cellPathsToReload = [cellPathsToReload subarrayWithRange:NSMakeRange(0, remainingPathCount - 1)];
// }
[table reloadRowsAtIndexPaths:cellPathsToReload withRowAnimation:UITableViewRowAnimationFade];
}
}
set the rowHeight property of UITableView.
documentation states:
The height of each row (table cell) in the receiver.
please provide you data source code
When using the following code to re-size a table row the last line of text is always cutoff, no matter how many lines there are. But there is white space added that looks like enough space for the text.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];
CGFloat restOfTheCellHeight = tableView.rowHeight - cell.detailTextLabel.frame.size.height;
CGSize constrainedSize = CGSizeMake(cell.detailTextLabel.frame.size.width, CGFLOAT_MAX);
CGSize textHeight = [cell.detailTextLabel.text sizeWithFont:cell.detailTextLabel.font constrainedToSize:constrainedSize lineBreakMode:cell.detailTextLabel.lineBreakMode];
CGFloat newCellHeight = (textHeight.height + restOfTheCellHeight);
if (tableView.rowHeight > newCellHeight) {
newCellHeight = tableView.rowHeight;
}
return newCellHeight;
}
Here is the code in cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomCellTableRowTypeSingleLineValueSmallLabel *cell = (CustomCellTableRowTypeSingleLineValueSmallLabel *)[tableView dequeueReusableCellWithIdentifier:#"CellTypeMultiLineLabelInCellSmallCell"];
if (cell == nil) {
NSArray *xibObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomCellTableRowTypeSingleLine" owner:nil options:nil];
for(id currentObject in xibObjects) {
if([currentObject isKindOfClass:[CustomCellTableRowTypeSingleLineValueSmallLabel class]]){
cell = (CustomCellTableRowTypeSingleLineValueSmallLabel *)currentObject;
}
}
cell.accessoryType = UITableViewCellAccessoryNone;
cell.editingAccessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
cell.detailTextLabel.lineBreakMode = UILineBreakModeWordWrap;
cell.detailTextLabel.numberOfLines = 0;
cell.detailTextLabel.text = self.attributeStringValue;
cell.textLabel.text = self.rowLabel;
return cell;
}
Any ideas?
You need to call [cell.detailTextLabel sizeToFit] in order for the label to actually resize in cellForRowAtIndexPath. It will not resize on its own just because you set numberOfLines to 0. See this question and read its answers for more clarification.
You are calculating the cell height appropriately in your heightForRowAtIndexPAth method, but then in your cellForRowAtIndexPath method you are never actually using it to set the height of your label within it.
So the table is allocating the right amount of space based on your heightForRowAtIndexPath, but then inserting into that space the unresized cell that you return from cellForRowAtIndexPath. I think this might the the cause of the problem and would explain the results you are seeing.
In cellForRowAtIndexPath you need to actually set the height of the label using the same calculation.
i.e.
CGSize constrainedSize = CGSizeMake(cell.detailTextLabel.frame.size.width, CGFLOAT_MAX);
CGRect cframe = cell.detailTextLabel.frame;
cframe.size.height = constrainedSize.height;
cell.detailTextLabel.frame = cframe;
You may also need to actually set the content view frame as well (not sure how it works with a non-custom cell).
I'm also not sure its a good idea to be calling cellForRowAtIndexPath from the heightForRowAtIndexPath method (it would probably be better to just directly access the text data you are using for the size calculation directly).
Turns out I just needed to enable all of the Autosizing options in interface builder for the label.
I have a list of data that I'm pulling from a web service. I refresh the data and I want to insert the data in the table view above the current data, but I want to keep my current scroll position in the tableview.
Right now I accomplish this by inserting a section above my current section, but it actually inserts, scrolls up, and then I have to manually scroll down. I tried disabling scrolling on the table before this, but that didn't work either.
This looks choppy and seems hacky. What is a better way to do this?
[tableView beginUpdates];
[tableView insertSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
NSUInteger iContentOffset = 200; //height of inserted rows
[tableView setContentOffset:CGPointMake(0, iContentOffset)];
If I understand your mission correctly,
I did it in this way:
if(self.tableView.contentOffset.y > ONE_OR_X_ROWS_HEIGHT_YOUDECIDE
{
self.delayOffset = self.tableView.contentOffset;
self.delayOffset = CGPointMake(self.delayOffset.x, self.delayOffset.y+ insertedRowCount * ONE_ROW_HEIGHT);
[self.tableView reloadData];
[self.tableView setContentOffset:self.delayOffset animated:NO];
}else
{
[self.tableView insertRowsAtIndexPath:indexPathArray WithRowAnimation:UITableViewRowAnimationTop];
}
With this code, If user is in the middle of the table and not the top, the uitableview will reload the new rows without animation and no scrolling.
If user is on the top of the table, he will see row insert animation.
Just pay attention in the code, I'm assuming the row's height are equal, if not , just calculate the height of all the new rows you are going to insert.
Hope that helps.
The best way I found to get my desired behavior is to not animate the insertion at all. The animations were causing the choppyness.
Instead I am calling:
[tableView reloadData];
// set the content offset to the height of inserted rows
// (2 rows * 44 points = 88 in this example)
[tableView setContentOffset:CGPointMake(0, 88)];
This makes the reload appear at the same time as the content offset change.
For further spectators looking for a Swift 3+ solution:
You need to save the current offset of the UITableView, then reload and then set the offset back on the UITableView.
func reloadTableView(_ tableView: UITableView) {
let contentOffset = tableView.contentOffset
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(contentOffset, animated: false)
}
Called by: reloadTableView(self.tableView)
Just call setContentOffset before endUpdates, that works for me.
[tableView setContentOffset:CGPointMake(0, iContentOffset)];
[tableView endUpdates];
I am using self sizing cells and the estimated row height was pretty useless because the cells can vary significantly in size. So calculating the contentOffset wasn't working for me.
The solution that I ended up with was quite simple and works perfectly.
So first up I should mention that I have some helper methods that allow me to get the data element for an index path, and the opposite - the index path for a data element.
-(void) loadMoreElements:(UIRefreshControl *) refreshControl {
NSIndexPath *topIndexPath = [NSIndexPath indexPathForRow:0 inSection:0]
id topElement = [myModel elementAtIndexPath:topIndexPath];
// Somewhere here you'll need to tell your model to get more data
[self.tableView reloadData];
NSIndexPath *indexPath = [myModel indexPathForElement:topElement];
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
[refreshControl endRefreshing];
}
None of the answers here really worked for me, so I came up with my solution. The idea is that when you pull down to refresh a table view (or load it asynchronously with new data) the new cells should silently come and sit on top of the tableview without disturbing the user's current offset. So here goes a solution that works (pretty much, with a caveat)
var indexpathsToReload: [IndexPath] = [] //this should contain the new indexPaths
var height: CGFloat = 0.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
UIView.performWithoutAnimation {
self.tableview.reloadSections(NSIndexSet(index: 0) as IndexSet, with: .none)
self.tableview.layoutIfNeeded()
indexpathsToReload.forEach({ (idx) in
height += self.feed.rectForRow(at: idx).height
})
let afterContentOffset = self.tableview.contentOffset
let newContentOffset = CGPoint(x: afterContentOffset.x, y: afterContentOffset.y + height)
self.tableview.setContentOffset(newContentOffset, animated: false)
}
}
CAVEAT (WARNING)
This technique will not work if your tableview is not "full" i.e. it only has a couple of cells in it. In that case you would need to also increase the contentSize of the tableview along with the contentOffset. I will update this answer once I figure that one out.
EXPLANATION:
Basically, we need to set the contentOffset of the tableView to a position where it was before the reload. To do this we simply calculate the total height of all the new cells that were added using a pre populated indexPath array (can be prepared when you obtain the new data and add them to the datasource), these are the indexPaths for the new cells. We then use the total height of all these new cell using rectForRow(at: indexPath), and set that as the y position of the contentOffset of the tableView after the reload. The DispatchQueue.main.asyncAfter is not necessary but I put it there because I just need to give the tableview some time to bounce back to it's original position since I am doing it on a "pull to refresh" way. Also note that in my case the afterContentOffset.y value is always 0 so I could have hard coded 0 there instead.