Load more items scrolling up on uitableview - ios

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;
}
};

Related

iOS infinite scroll items disapearing (sometimes)

This is a very rare occurring bug from hell,
I have an infinite scroll controller that displays products, 2 in each row. Rarely, something affects the controller and causes items to vanish, when I tap the empty area where the item should be, it works as expected and directs the user to the item details controller. When I back out back to the list, sometimes the cell shows its content, and others get hidden.
Sometimes it just a couple of items missing, sometimes there are so many missing items that makes the list appear empty, like only 1 or 2 cells are visible per screen height.
An even stranger situation is, when I scroll really fast to the end and stretch the screen really fast out of the visible area, and there are no more items to load, the visible items can jump from left to right.
Please see these two videos.
https://www.dropbox.com/s/jibflcouz1ena8n/missingProductImages.mov?dl=0
https://www.dropbox.com/s/uz13fzorypnp38t/again.mov?dl=0
I could send code but I didn't want to clutter this place with full length code, let me know if you want to see a specific section of the code please. Maybe someone could have an idea of what might be going on by looking at the vids.
What's probably going on here is a state problem with your collection view cells. The code assigning model values to the cell's views would be of use here. But absent any actual information, the first thing to review would be the cell's prepareForReuse implementation:
Does it call super?
Does it clear out all current values?
Does it cancel any pending asynchronous operations?
Are fetches and cancellations correctly matched?
Next, check if there's any essential configuration in the cell's init/initWithCoder methods -- those are only called on first creation, not on reuse.
Those are the normal pitfalls of UICollectionView cell handling. If they don't suggest the problem, please post the cell code.
It looks like your cells are not being reused correctly.
Can you check that you have set the same reuseIdentifier for your cell in Interface Builder that you are assigning in your code?
Update: Attached image to show where to set the identifier in the storyboard/xib
Update: added layout solution
Another problem could be due to the layout bounds of your collectionViewCell. When you load your cells they bounds are not calculated until they have been added to your collectionView and rendered. This can cause the elements in your cell to layout with the wrong values. This happens commonly with async image frameworks as they cache a resized version of the image for performance. When you cell loads, the cached image is loaded at the wrong the wrong size.
Add the following code to your collectionViewCell:
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
self.contentView.frame = bounds;
[self setHighlighted:NO];
}

UITableView Load records with pagination by removing existing records

Actually I want to show records in UITableView, total number of records may exceed 100 Thousand. So I am loading these records in chunks of 100 records every time when users reaches scrolling upto 70% records(row no.70).
I don't want to keep existing chunk of records in memory, So initially I am loading 2 chunks of 100 records, and when user scroll reaches to Row no.170, I remove first 100 records and loading 1 chunk of 100 records.
But my problem is how do I reset value of indexPath? as currently its value is 170, but after loading new chunk of 100 records and removing first 100 records, I want its value to be 70.
I have found workaround to above problem by using following line of code.
[tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:70 inSection:indexPath.section] atScrollPosition:UITableViewScrollPositionBottom animated:NO];
But above line of code stops scroll suddenly. So it appears like scroll is stopped suddenly at Row no.70.
Is there any other solution to implement above problem, Thanks in advance.
Why not allow the user to access the data in some other way, such as:
Search functionality
Subcategorisation of data
It's perhaps worth asking, since it's such a large amount of data, is it really likely your user will scroll that far down, and if they have to, will it impact user experience negatively?
For arguments sake, if you wanted to protect your memory use, you would have to dynamically add and remove cells depending on offset. The standard UITableview methods do a pretty good job of doing that for you.
In any case, the number of cells you have allocated at any one time should take into consideration what you are putting into your cells, e.g. images vs. text. If it's images, then 100k is going to lead to a massive application bundle (unless you pull them from your server as needed). If it's text, just go with Apple's own UITableView methods, as they will be more efficient.
But if you're adamant about this approach, and wanted to roll your own custom fix, you could layout a load of UIViews in a UIScrollView, and use the contentOffset of the UIScrollView to know when to add and remove cells. The actual adding of the cells should be done off-screen.
A. In scrollViewDidScroll, watch for specific offsets aligned with cellHeights, e.g.
if(fabsf(y)%200==0){
//add new cells below, drop old cells at the top
}
as in, if your cells were 50px in height, it would hit this method every 4 cells.
B. Update your dynamic indexPath each time new cells are added / old ones are removed. It would be the indexPath of the uppermost cell. Based on that, you can calculate the number of cells currently 'allocated' and the points at which you should pull your cell data from an array:
for (int n = dynamicIndexPath; n<dynamicIndexPath+numberOfCells; n++){
//this is the window onto your data.
}
When adding e.g. 4 new cells:
for (int n = dynamicIndexPath + numberOfCells-4; n<4; n++){
//[self layoutNewCellWithData:massiveArray[n] atOffset:y];
}
This would be a sort of homegrown UITableview (UIViews in a UIScrollView) where you handle the dequeuing yourself.
But, if it were me, I would look at an alternative way to attack the data, such as via search or some way of splitting the data up into smaller groups. Failing that I would use Apple's UITableView methods, failing that roll my own.

How to display x number of objects from array in a UITableView, then display more on scroll down?

I'm working on an a game app in swift that currently has a tableView displaying the scores, number of trys etc, downloaded in an array from parse.
However, this table can get pretty long if the user plays the game many times. So I'd like to improve the app by displaying the first, say, 20 objects of the array in a tableview, then, once the user scrolls down to the end of the table it automatically adds more rows and displays the next 20 objects in the array (along with the original 20, making the tableview now 40 rows)
If anybody's familiar with the twitter app, that's exactly what I'd like to go for. There's a set amount of tweets shown initially, then once you scroll down to the end of the table more tweets are loaded, in order to decrease loading times.
Problem is, I really have no clue how to implement this at all. I've never been in the situation where I only need to use part of an array. Any help would be greatly appreciated. Dan
UITableView is a virtual view where it calls you back for the data to create cells for a given row that's in view and destroy's cells that go out of view. You should also re-use cells that have been created.
So, I think the answer to your question about pre-loading portions of the list is ... you shouldn't have to. Just implement the data source and answer the data call backs.
More here in the apple docs on the datasource for a UITableView
This is addressed in How to know when UITableView did scroll to bottom in iPhone in Objective-C. If any help needed translating this to Swift, post as a comment and I'll update answer.
EDIT -- The real question:
The tableView delegate methods allow you to perform arbitrary logic / mapping between your data and the table's viewable data. So when you do detect a scroll to the bottom, increment an internal state variable, such as var rowsToReveal declared as a class-wide stored property. After incrementing, call tableView.reloadData(). Then re-write numberOfRowsInSection delegate method to use rowsToReveal rather than someDataArray.count or whatever hardcoded value you had. Make sure rowsToReveal never exceeds the count, of course, otherwise -- instant crash.

UICollectionView insertItemsAtIndexPaths: seems to reload collectionview or autoscrolls to top

I am using a UICollectionView in combination with a NSFetchedResultsController. When I fetch new data, I throw them in a SQLite database and my frc gets a notification that it will update. I implement the appropriate delegate methods and insert the new data in a batch update and I don't call reloadData afterwards. But as soon as the data is inserted, the collectionview scrolls to the top (to make the new items visible?). I would like to interfere this process and imitate the Facebook/9Gag implementation. When there is new data, a button is faded in which lets the user scroll to the top. At the time the button is shown, the new items are already inserted, so the user can also just scroll up himself. I tried the following:
hooking into the UIScrollViewDelegate methods to see if I could stop the collectionView from scrolling (none of the methods is called during this update process)
show the "scroll to show more"-button when the frc willUpdate and just insert the items when the button is pressed. The problem is that the user cannot scroll up himself
Before inserting the items, I calculate the indexPath of currently visible items AFTER the insertion (adding the number of inserted items) and scroll to that index without animation in the completion block of the batch update. This only works if I delay the scrolling, but then it flickers, because it's first scrolling up a little
I experimented the same problem, by calling "insertItemAtIndexPaths" to increment data into my collectionView.
I noticed two things :
- If you add items AFTER the current visible index path, there's no automatic scroll
- If you add items BEFORE the current visible index path, there's no automatic scroll, like you said, but the contentOffset is kept at the same position, but with different contentSize, because you've just added new items.
To deal with this "side effect", I used the solution mentioned here : UICollectionView insert cells above maintaining position (like Messages.app)
As it's explained in the following post, the "CATransaction.setDisableAction(false)" is the key thing to update smoothly the contentOffset position.

Resize TableCell on select/deselect

I'm new to iOS and MonoTouch, and I have a pretty basic need - I have two vastly different views to display in the same table cell depending on whether it is selected or not. When you load the app I'll present a list of products in a custom UITableView, with all the rows deselected and each row basically just showing the product names in a label. As soon as a user taps one of the rows and selects it, I show a very different view with more of a shopping cart layout. Since the two views are so different they have vastly different sizes. What i need is for the cell itself (or row?) to grow and shrink according to the natural height of whichever view is currently being displayed.
I'm using descendants of UITableViewController, UITableViewSource, and UITableViewCell for the solution. What I do is add both versions to the cell's ContentView, then set the Hidden property as needed when the cell/row is selected/deselected to show the right layout. That works well. I'm in demo mode right now, so I'm not overly worried about efficiency or responsiveness, that will have to come later.
I know that GetHeightForRow is responsible for sizing the rows but it only gets called once, when the cell is first shown. It seems to me that I need to alert the TableView to re-poll the source for a new size as the views are changing, but I can't figure out how to do it.
I tried (rather hopefully) to cause GetHeightForRow to be invoked again manually using Cell.SetNeedsLayout and Cell.SetNeedsDisplay, hoping that would cause the table to re-query the source for new dimensions, but no joy.
I tinkered with the direct approach, trying to size the contentview itself but it didn't seem as if it was leading anywhere. I feel as if the table needs to be told to query for a new row size, but I'm open to any suggestions.
Surely I'm not the first to attempt this? What am I missing?
Try forcing GetHeightForRow to be called again by calling ReloadRows instead.
You may or may not have to specify begin/end updates (this might just be for animation, though?)
tableView.BeginUpdates();
tableView.EndUpdates();

Resources