I'm making a UITableView
with comments. I have the latests comments on the bottom, and the older ones at the top.
I'm saving the comments in an NSMutableArray
. When i reach the top i want to load more older comments. How can i do this?
I'm making a UITableView
with comments. I have the latests comments on the bottom, and the older ones at the top.
I'm saving the comments in an NSMutableArray
. When i reach the top i want to load more older comments. How can i do this?
You need a few methods:
Fetch Method - This depends on how you are implementing the data retrieval. Say by default you only show the most recent 10 comments. If your array contains only these 10 comments, then you need a mechanism to perform a fetch of either a local or a remote database to pull another 10 additional comments and add them to the array. If though the array always contains all comments related to that post (say 56 comments in this case), the additional fetch is not necessary, though one could argue that retrieving and storing comments that users might never see is inefficient. Some math will need to be carried out in order to display the right amount of comments, but I will get to this in a little bit.
Updating the Table -
You will want to call [tableView reloadData]
when the top cell is viewed because there will be more data to show. Depending on whether the array contains only the comments to be displayed or all comments related to the post, you may need to call the fetch method above as well. For example, if your array contains only 10 comments and you need to pull the additional 10, then you will first need to perform the fetch, and when the fetch completes you will need to call reloadData
. If the array contains all 56 comments, then you will need to update the numCommentsToShow
property as described below, and then immediately call reloadData
Update Trigger -
The method that reloads the table needs to be called when the top cell is viewed; however, you do not want to call the reload table right away when the view first appears. There are numerous ways of accomplishing this, and it really depends on if you want to additional comments to appear when the top cell is partially visible, fully visible, or even about to be viewed (which actually might be the cleanest for the user). One easy implementation (but not necessarily the most ideal) is to include the call to update the table in the cellForIndexPath
method. If indexPath.row
is 0 then call update. You will want to first test though that the view was not just loaded before allowing the update. You will also want the cell returned to the contain the comment that would have appeared before the update, because the cellForIndexPath
method will be called again after the fetch. If you want the cell to be fully visible before allowing the update, you could do something like this: Best way to check if UITableViewCell is completely visible
The Math -
If the array only contains the comments to be shown, I would actually make the NSMutableArray
an NSArray
. When you get the results of the fetch, I would just hold those 20 comments in a completely new array, and have the comment array property just reference the new array. Then call reloadData
. Otherwise, adding the comments to an NSMutableArray
while the user is scrolling sounds a bit dangerous to me, but I could be wrong. Btw, some people might actually only perform the fetch on the 10 next comments (i.e. 11-20), but I would retrieve 1-20 if you are allows the users to edit their comments since the most recent comments are the ones most likely to change. After the fetch, the array will contain the 20 comments, and they should be in order of least to most recent. This makes it easy for the cellForIndexPath
method, which should just retrieve the comment at index indexPath.row
for each cell. The cell currently being viewed (the same one before the update) would be at the index equal to the number of new items just pulled.
If the array contains all comments related to the post (56 in my example), then you need to have a property that contains the number of comments to be displayed at this given point in time. When the view first appears, this property is set to some default number (10 in my example). The numberOfRowsInSection
method needs to return this property which I will call numCommentsToShow
. However, the cellForIndexPath
needs to retrieve the correct items in the array. If they sorted from least recent to most recent, you actually need to retrieve the comment at the index equal to the number of comments in the array less numCommentsToShow
plus indexPath.row
. It gets a bit confusing doing it this way. When you call the update, you need to increase numCommentsToShow
by some incremental amount (in my example) 10, but you should be careful to not increase it to greater than the number of items in the array. So you should actually increase it by the minimum of the incremental amount and the amount of comments not yet shown.
Issues with Scrolling You probably will encounter some issues while scrolling since the cell at the top of the table will actually become one of the middle cells of the table. Thus, the table will likely jump to the top after the update. There is some math that you can do to ensure that the scroll position of the table after the update is set so that it appears to the user that the position of the scroll has not changed, when in reality it has. And there is an even more difficult challenge that will occur if the user moves the table down while the table has not yet updated.
Before explaining how to do this, the trend these days seems to be avoiding this situation. If you look at the facebook app, there is never really a infinite scroll to the top. There is a button to load the newer posts that automatically scrolls to the top. And comments on posts start at the top with the least recent comment and you can click a button to "view more comments" to see more recent ones. Not to say that this is good user experience, but programming the fix might be more time confusing than it's worth.
But anyway, to get around the scroll issue, I would use the setContentOffset
method since a UITableView
inherits from UIScrollView
. You should not use the scrollToRowAtIndexPath
method of a UITableView
since it is possible that the current scroll position might not be completely lined up with any given row. You should make sure that the animated
parameter is set to NO
. The exact position though that you want to scroll to involves a bit of math and timing. It is possible that the user has scrolled to a new position while the data was being fetched, so you have offset the current position, not whatever the position was before the fetch. When the fetch is complete and you reload the table, you need to immediately call setContentOffset
. The new y position needs to be the current scroll y position + to the number of new comments to show * the row height.
There is a bit of a problem though. It is possible that the view will be in the middle of scrolling when the fetch is complete. Calling setContentOffset
at this point will probably cause a jump or a stall in the scrolling. There are a few ways of handling this case. If the user has scrolled away from the top of the table, then perhaps he/she might not want to view those records anymore. So perhaps you ignore the data you fetched or save it in another place without including the results in the table. Another is to wait until the table is finished scrolling before using the new data. The third way to continue scrolling with the same velocity and deceleration as you were before the table was reloaded, but I am not sure how you would accomplish this offhand.
To me this is more pain than it's worth.
Hope this helps,
Here is my solution in Xamarin, with code to handle the scroll issue. My header is a UIActivityIndicator
, which is removed when there is no more content to load. DidScroll
is an event which is fired in the Scrolled method of the UITableViewSource
. DidScrollEventArgs
contains the UIScrollView
passed from the Scrolled method.
So in the UITableViewSource
:
public class DidScrollEventArgs : EventArgs
{
public UIScrollView scrollView;
public DidScrollEventArgs (UIScrollView scrollView) : base ()
{
this.scrollView = scrollView;
}
}
public event EventHandler<DidScrollEventArgs> DidScroll;
public override void Scrolled (UIScrollView scrollView)
{
if (DidScroll != null)
DidScroll.Invoke (this, new DidScrollEventArgs (scrollView));
}
And in my UIViewController
:
source.DidScroll += async (object sender, MessageSource.DidScrollEventArgs e) => {
nfloat height = e.scrollView.ContentSize.Height;
nfloat contentYoffset = e.scrollView.ContentOffset.Y;
if (contentYoffset == 0 && source.model.Count > 0 && height > MyBounds.Height && !isLoading && isMore) {
isLoading = true;
DateTime cutOff = source.model[0].SentDate.Value;
//fetch more content
List<MessageModel> newModels = await MessageAccess.GetMessagesAsync(ThreadId, cutOff, true);
if (newModels.Count == 0){
isMore = false;
tableView.TableHeaderView = null;
isLoading = false;
return;
}
//add it on to the front of the current content
models = newModels.Concat(source.model).ToList();
source.model = models;
tableView.ReloadData();
//calculate where to scroll to (new height - old height)
nfloat yOffset = tableView.ContentSize.Height - height;
//scroll there
tableView.SetContentOffset(new CGPoint(0, yOffset), false);
isLoading = false;
}
};