Large UICollectionViewCell stopped being displayed when scrolling - iphone

The same behavior of UICollectionView as described here has been led to this question. Even though I decided to post my own one, because I did further investigations, I didn't want to post in a comment or in edit of the question mentioned above.
What happens?:
When large cells being displayed in a UICollectionView with a UICollectionViewFlowLayout, after scrolling the collection view to a certain offset, the cells will disappear.
When scrolling further until another cell comes into visible area, the vanished/hidden cell becomes visible again.
I tested with a vertical scrolling collection view and full-width-cells, but I'm rather sure, that it would also happen with similar setups for horizontal scrolling.
What are large cells?:
The described behavior happens with cells higher than twice the display height (960.f + 1.f on 3,5 inch displays, 1136.f + 1.f on 4 inch).
What exactly happens?:
When the scrolling offset of the collection view exceeds cell.frame.origin.y + displayHeightOfHardware the cells hidden property is set to YES and -collectionView:didEndDisplayingCell:forItemAtIndexPath: gets called (e.g. the first cell changes to hidden when scrollingOffset.y reaches 481.f on 3,5-inch-iPhone).
As described above, when scrolling until next cell comes into view, the hidden cell gets displayed again (i.e. hidden property changes to NO) and furthermore, when scrolling far enough the cell will never vanish again, when it shouldn't, no matter where you scroll to.
This changes when working with cells larger than triple-display-height (1441.f/1705.f). Those show the same behavior, but it stays the same, no matter how far they're being scrolled up and down.
What else?:
The situation can not be fixed by overriding -(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds to return YES.
The cells cannot being forced to display with setting the hidden property to NO programmatically after they were hidden (in didEndDisplayingCell for example)
So, whats the question?:
I'm pretty sure, that this is a bug in UICollectionView/Controller/Cell/Layout and I'll submit a TSI at Apple. But for the meantime: Has anyone any ideas for a quick hack solution?

i have a VERY dirty and internal solution for this problem:
#interface UICollectionView ()
- (CGRect)_visibleBounds;
#end
#interface MyCollectionView : UICollectionView
#end
#implementation MyCollectionView
- (CGRect)_visibleBounds {
CGRect rect = [super _visibleBounds];
rect.size.height = [self heightOfLargestVisibleCell];
return rect;
}
- (float)heightOfLargestVisibleCell {
// do your calculations for current max cellHeight and return it
return 1234;
}
#end

I have a workaround that seems to be working for me and should not run amok of Apple's rules for iOS applications.
The key is the observation that the large cells bounds are the issue. I've worked around that by ensuring that one edge of the cell is within the viewable area of the scrollable content region. You'll obviously need to subclass the UICollectionViewFlowLayout class or UICollectionViewLayout depending on your needs and make use of the contentOffset value to track where you are in the UIScrollView.
I also had to ensure:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
returns YES or face a runtime exception indicating the layout was invalid. I keep the edge of the larger cell bound to the left edge in my case. This way you can avoid the erroneous bounds intersection detection for these larger cells.
This does create more work depending on how you would like the contents of the cell to be rendered as the width/height of the cell is being updated as you scroll. In my case, the subviews within the cell are relatively simple and do not require a lot of fiddling with.
As requested here is an example of my layoutAttributesInRect
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray* attributes = [NSMutableArray array];
NSArray *vertical = myVerticalCellsStore.cells;
NSInteger startRow = floor(rect.origin.y * (vertical.count)/ (vertical.count * verticalViewHeight + verticalViewSpacing * 2));
startRow = (startRow < 0) ? 0 : startRow;
for (NSInteger i = startRow; i < vertical.count && (rect.origin.y + rect.size.height >= i * verticalViewHeight); i++) {
NSArray *horizontals = myHorizontalStore.horizontalCells;
UICollectionViewLayoutAttributes *verticalAttr = [self layoutAttributesForSupplementaryViewOfKind:#"vertical" atIndexPath:[NSIndexPath indexPathForItem:0 inSection:i]];
if (CGRectIntersectsRect(verticalAttr.frame, rect)) {
[attributes addObject:verticalAttr];
}
BOOL foundAnElement = NO;
for (NSInteger j = 0 ; j < horizontals.count; j++) {
MYViewLayoutAttributes *attr = (MyViewLayoutAttributes *)[self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:j inSection:i]];
if (CGRectIntersectsRect(rect, attr.frame)) {
[attributes addObject: attr];
foundAnElement = YES;
}
else if (foundAnElement) {
break;
}
}
}
return attributes;
}
This is my sanitized code. Basically I calculate about were the first cell should be based on the cell height. In my case that is fixed, so the calculation is pretty easy. But my horizontal elements have various widths. So the inner loop is really about figuring out the right number of horizontal cells to include in the attributes array. There I'm using the CGRectIntersectsRect to determine if the cell intersects. Then the loop keeps going until the intersection fails. And if at least one horizontal cell has been found the loop will break. Hope that helps.

My solution is basically the same as Jonathan's but in a category, so you don't have to use your own subclass.
#implementation UICollectionView (MTDFixDisappearingCellBug)
+ (void)load {
NSError *error = nil;
NSString *visibleBoundsSelector = [NSString stringWithFormat:#"%#isib%#unds", #"_v",#"leBo"];
if (![[self class] swizzleMethod:NSSelectorFromString(visibleBoundsSelector) withMethod:#selector(mtd_visibleBounds) error:&error]) {
FKLogErrorVariables(error);
}
}
- (CGRect)mtd_visibleBounds {
CGRect bounds = [self mtd_visibleBounds]; // swizzled, no infinite loop
MTDDiscussCollectionViewLayout *layout = [MTDDiscussCollectionViewLayout castedObjectOrNil:self.collectionViewLayout];
// Don`t ask me why, but there's a visual glitch when the collection view is scrolled to the top and the max height is too big,
// this fixes it
if (bounds.origin.y <= 0.f) {
return bounds;
}
bounds.size.height = MAX(bounds.size.height, layout.maxColumnHeight);
return bounds;
}
#end

I found that this issue only occurred when using a subclassed UICollectionViewLayoutAttributes and when that attribute class did not have a correct isEqual: method.
So for example:
#implementation COGridCollectionViewLayoutAttributes
- (id)copyWithZone:(NSZone *)zone
{
COGridCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
attributes.isInEditMode = _isInEditMode;
return attributes;
}
- (BOOL)isEqual:(id)other {
if (other == self) {
return YES;
}
if (!other || ![[other class] isEqual:[self class]]) {
return NO;
}
if ([((COGridCollectionViewLayoutAttributes *) other) isInEditMode] != [self isInEditMode]) {
return NO;
}
return [super isEqual:other];
}
#end
Worked but originally I had:
return YES;
This is on iOS 7.

Related

Create infinite scroll effect by inserting new rows [duplicate]

How do I do an infinite scrolling in a UITableView? I know how to do it using a UIScrollView, in which apple has demonstrated in one of the WWDC's video. I tried doing the following in tableView:cellForRowAtIndexPath::
if (indexPath.row == [self.newsFeedData_ count] - 1)
{
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
but this fails. Any other idea?
If you need to know when you hit the bottom of the UITableView, become it's delegate (because it is a subclass of UIScrollView), and use the -scrollViewDidScroll: delegate method to compare the table's content height and it's actual scroll position.
EDIT (something like this):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat actualPosition = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height - (someArbitraryNumber);
if (actualPosition >= contentHeight) {
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
}
You can support infinite scroll with pull to refresh at the top and/or scroll continuously at the bottom with a spinner wheel using:
https://github.com/samvermette/SVPullToRefresh
SVPullToRefresh handles the logic when UITableView reaches the bottom. A spinner is shown automatically and a callback block is fired. You add in your business logic to the callback block.
Here's an example:
#import "UIScrollView+SVInfiniteScrolling.h"
// ...
[tableView addInfiniteScrollingWithActionHandler:^{
// append data to data source, insert new cells at the end of table view
// call [tableView.infiniteScrollingView stopAnimating] when done
}];
This project can be added to your project using CocoaPods or directly compiled into your project.
Here's a very quick and complete demo of an infinite scrolling UITableView I put together...
#interface InfiniteScrollViewController ()
#property (nonatomic) NSMutableArray *tableViewData;
#property (nonatomic) BOOL loadingMoreTableViewData;
#end
#implementation InfiniteScrollViewController
- (void)viewDidLoad {
self.tableViewData = [[NSMutableArray alloc] init];
[self addSomeMoreEntriesToTableView];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.tableViewData.count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if (indexPath.row < self.tableViewData.count) {
cell.textLabel.text = [self.tableViewData objectAtIndex:indexPath.row];
} else {
cell.textLabel.text = #"Loading more data...";
// User has scrolled to the bottom of the list of available data so simulate loading some more if we aren't already
if (!self.loadingMoreTableViewData) {
self.loadingMoreTableViewData = YES;
[self performSelector:#selector(addSomeMoreEntriesToTableView) withObject:nil afterDelay:5.0f];
}
}
return cell;
}
- (void)addSomeMoreEntriesToTableView {
int loopTill = self.tableViewData.count + 20;
while (self.tableViewData.count < loopTill) {
[self.tableViewData addObject:[NSString stringWithFormat:#"%i", self.tableViewData.count]];
};
self.loadingMoreTableViewData = NO;
[self.tableView reloadData];
}
#end
'UITableView' is same as 'UIScrollView' in 'scrollViewDidScroll' method.
So, its easy to emulate infinite scrolling.
double the array so that head and tail are joined together to emulate circular table
use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.
:
/* To emulate infinite scrolling...
The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualising joined table in eight parts)
When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.
Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)
In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
*/
-(void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat currentOffsetX = scrollView_.contentOffset.x;
CGFloat currentOffSetY = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height;
if (currentOffSetY < (contentHeight / 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
}
if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
}
}
P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8
If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit the method when data count is say for example less than 9.
For me worked better scrollViewDidEndDragging: than scrollViewDidScroll:.
The second approach will send you each position during scroll and cause, if you are fetching remote resources you will hit your endpoint several times, which is not good.
Complete example based on #codafi solution with comments from #danielgomezrico about how to calculate contentHeight:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate {
CGFloat actualPosition = scrollView.contentOffset.y;
CGFloat contentHeight = scrollView.contentSize.height - (self.tableView.frame.size.height);
if (actualPosition >= contentHeight) {
// fetch resources
[self.tableView reloadData];
}
}
Generally I override scrollViewDidEndDecelerating and inside it I put my code to request more data.
Example:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height){
//put here your code
}
}
Recently I uploaded on GitHub a subclass of UITableView, that implements the infinite scroll.
You can download it here:
https://github.com/alchimya/iOS-LazyTableView
rather than overriding we can do this optimally in layoutSubviews.
Here's how I got it implemented. You can get to know more about the implementation here
- (void)layoutSubviews{
[super layoutSubviews];
if(self.delegateForViews){
CGPoint contentOffset = self.contentOffset;
if([self.delegateForViews noOfViews]>numOfReusableViews){
NSUInteger centerIndex=visibleViews.count/2;
NSUInteger noOfViews=[self.delegateForViews noOfViews];
UIView *centerView=[visibleViews objectAtIndex:centerIndex];
CGPoint centerViewOrigin=centerView.frame.origin;
CGSize centerViewSize=centerView.frame.size;
CGFloat offsetDifference=contentOffset.x-centerViewOrigin.x;
CGFloat offsetDifferenceAbs=fabs(contentOffset.x-centerViewOrigin.x);
if(offsetDifferenceAbs>=centerViewSize.width){
if(offsetDifference<0){
currentPosition--;
}else{
currentPosition++;
}
self.contentOffset=centerViewOrigin;
currentPosition=[self getPosition:currentPosition noOfViews:noOfViews];
[self.delegateForViews clearView:centerView];
[self.delegateForViews setupView:centerView forPosition:currentPosition];
for (int i=centerIndex-1; i>=0; i--) {
UIView* prevView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:prevView];
[self.delegateForViews setupView:prevView forPosition:
[self getPosition:currentPosition-1 noOfViews:noOfViews]];
}
for (int i=centerIndex+1; i<visibleViews.count; i++) {
UIView* nextView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:nextView];
[self.delegateForViews setupView:nextView forPosition:
[self getPosition:currentPosition+1 noOfViews:noOfViews]];
}
}
}
}
}
One of the simple and that offered me everything i need is this class:
https://github.com/jakemarsh/JMStatefulTableViewController
You just need to subclass JMStatefulTableViewController and the it has 3 methods that you need to overwrite:
one that is called on init, to get the initial data
statefulTableViewControllerWillBeginInitialLoading
one when the user pull to refresh
statefulTableViewControllerWillBeginLoadingFromPullToRefresh
one when is called for the infinite scroll (next page)
statefulTableViewControllerWillBeginLoadingNextPage
This can be used from Cocoapods too.
scrollviewDidScroll will call when you move through the rows in tableview
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//check for the visible rows
let indexpath = self.tableView.indexPathsForVisibleRows?.last
//check if the visible row last is equal to the total number of counts
if(indexpath?.last == self.listCount){
//code for adding data to the tableview and reload the table view.
}
}
look in the link for more details about indexPathForVisibleRows
https://developer.apple.com/documentation/uikit/uitableview/1614885-indexpathsforvisiblerows

Overriding layoutAttributesForElementsInRect: of UICollectionView / PSTCollectionView causes visible cell to reload

I want to mimic the features of gallery app provided in Sony Xperia in iPhone app. I gallary app, images are shown in grid grouped by date and the first photo of the section is twice in size than the others. On pinching out / in, all photos get zoom out / in.
I used PSTCollectionView as suggested by Lithu T.V and have created a custom layout. In that layout I have overridden - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect. Bellow is the code for same.
// called continuously as the rect changes
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attribs = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *arrmFrames=nil;
for (int i=0;i<attribs.count;i++) {
UICollectionViewLayoutAttributes *attributesInitial=[attribs objectAtIndex:0];
UICollectionViewLayoutAttributes *attributes=[attribs objectAtIndex:i];
//Take initial frame from first cell
if(i==0)
fFirstCellsY = attributes.frame.origin.y;
//while Y is constant, save the adjusted frames for next cells
else if(attributes.frame.origin.y<fFirstCellsY+attributesInitial.frame.size.height)
{
if(arrmFrames==nil)
arrmFrames=[[NSMutableArray alloc]init];
attributes.frame=CGRectMake(attributes.frame.origin.x, attributesInitial.frame.origin.y, attributes.frame.size.width, attributes.frame.size.height);
[arrmFrames addObject:NSStringFromCGRect(CGRectMake(attributes.frame.origin.x, attributes.frame.origin.y+attributes.frame.size.height+10, attributes.frame.size.width, attributes.frame.size.height))];
}
//Adjust the frame of other cells
else
{
CGRect frame = attributes.frame;
attributes.frame=CGRectFromString((NSString*)[arrmFrames objectAtIndex:0]);
[arrmFrames removeObjectAtIndex:0];
[arrmFrames addObject:NSStringFromCGRect(frame)];
}
}
}
return attribs;
}
This works and the layout is looking like I wanted.
By this method more cells become visible than those by using default layout and my layout looks OK. but when I scroll down then some visible cells are getting reloaded. I guess the reason is the delegate method - (PSUICollectionViewCell *)collectionView:(PSUICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath is getting called before - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect which is there in my custom layout. and so the cells which were not visible in default layout but visible in custom layout are reloaded late.
How can I overcome this?
Use UICollectionView
If app is supposed to support in less than 5.0 use PSTCollectionView
A nice startup

UICollectionView animations (insert/delete items)

I'd like to customize the animation styles when a UICollectionViewCell is inserted and/or deleted.
The reason why I need this is that by default I see that inserting a cell has a smooth fade in animation, however deleting a cell has a combination of move-to-the-left + fade out animation. I would be very happy with this if not for one problem.
After I delete a cell, it is still reused when I add new ones, and when it's reused it's added not with the default fade in effect, but instead it's a combination of move-to-the-left + fade in.
I'm not sure why I'm getting this inconsistency in animations. If this is a known bug/problem/stupidity(on my side :)) please let me know how to fix it.
Otherwise, let me know how to set custom animations when the cell is deleted (or point me towards a tutorial).
Thanks
UPDATE
Fixed the weird animation behavior by subclassing UICollectionViewFlowLayout and adding this line of code
- (UICollectionViewLayoutAttributes *) initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
return nil;
}
That's it! :)
If you use your own subclass of UICollectionViewLayout, you can implement the methods:
initialLayoutAttributesForAppearingItemAtIndexPath: for insertions
finalLayoutAttributesForDisappearingItemAtIndexPath: for deletions
According to the documentation, the attributes you return are used as starting points for the animation, and the end point are the normal attributes returned by your layout (or the opposite for deletion). Layout attributes include position, alpha, transform...
Of course, it is more work to write your own layout class than to use the Apple provided flow layout.
Edit: To answer your question in the comments, here is a super basic implementation of a layout for rows of items which are all the same size.
A cell has a frame and, by default, an alpha of 1.0 (as defined by layoutAttributesForItemAtIndexPath:). When it is deleted, its properties will be animated from its current state before the deletion to the properties set by finalLayoutAttributesForDisappearingItemAtIndexPath:, which correspond to the same frame and an alpha of 0.0. So it won't move but it will fade out. However, the cells to the right are going to be moved to the left (because their indexPath has changed, and thus their frame as set by layoutAttributesForItemAtIndexPath:).
- (CGSize)collectionViewContentSize
{
NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:0];
return CGSizeMake(numberOfItems * ITEM_WIDTH, ITEM_HEIGHT);
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger index = [indexPath indexAtPosition:0];
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
attributes.frame = CGRectMake(index * ITEM_WIDTH, 0, ITEM_WIDTH, ITEM_HEIGHT);
return attributes;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *attributes = [NSMutableArray new];
NSUInteger firstIndex = floorf(CGRectGetMinX(rect) / ITEM_WIDTH);
NSUInteger lastIndex = ceilf(CGRectGetMaxX(rect) / ITEM_WIDTH);
for (NSUInteger index = firstIndex; index <= lastIndex; index++) {
NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndexes:(NSUInteger [2]){ 0, index } length:2];
[attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
return attributes;
}
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
attributes.alpha = 0.0;
return attributes;
}
Download the circle Layout. It is a sample custom layout that using
initialLayoutAttributesForAppearingItemAtIndexPath:
finalLayoutAttributesForDisappearingItemAtIndexPath:
That will be a good working material for you.

UITableView infinite scrolling

How do I do an infinite scrolling in a UITableView? I know how to do it using a UIScrollView, in which apple has demonstrated in one of the WWDC's video. I tried doing the following in tableView:cellForRowAtIndexPath::
if (indexPath.row == [self.newsFeedData_ count] - 1)
{
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
but this fails. Any other idea?
If you need to know when you hit the bottom of the UITableView, become it's delegate (because it is a subclass of UIScrollView), and use the -scrollViewDidScroll: delegate method to compare the table's content height and it's actual scroll position.
EDIT (something like this):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat actualPosition = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height - (someArbitraryNumber);
if (actualPosition >= contentHeight) {
[self.newsFeedData_ addObjectsFromArray:self.newsFeedData_];
[self.tableView reloadData];
}
}
You can support infinite scroll with pull to refresh at the top and/or scroll continuously at the bottom with a spinner wheel using:
https://github.com/samvermette/SVPullToRefresh
SVPullToRefresh handles the logic when UITableView reaches the bottom. A spinner is shown automatically and a callback block is fired. You add in your business logic to the callback block.
Here's an example:
#import "UIScrollView+SVInfiniteScrolling.h"
// ...
[tableView addInfiniteScrollingWithActionHandler:^{
// append data to data source, insert new cells at the end of table view
// call [tableView.infiniteScrollingView stopAnimating] when done
}];
This project can be added to your project using CocoaPods or directly compiled into your project.
Here's a very quick and complete demo of an infinite scrolling UITableView I put together...
#interface InfiniteScrollViewController ()
#property (nonatomic) NSMutableArray *tableViewData;
#property (nonatomic) BOOL loadingMoreTableViewData;
#end
#implementation InfiniteScrollViewController
- (void)viewDidLoad {
self.tableViewData = [[NSMutableArray alloc] init];
[self addSomeMoreEntriesToTableView];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.tableViewData.count + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if (indexPath.row < self.tableViewData.count) {
cell.textLabel.text = [self.tableViewData objectAtIndex:indexPath.row];
} else {
cell.textLabel.text = #"Loading more data...";
// User has scrolled to the bottom of the list of available data so simulate loading some more if we aren't already
if (!self.loadingMoreTableViewData) {
self.loadingMoreTableViewData = YES;
[self performSelector:#selector(addSomeMoreEntriesToTableView) withObject:nil afterDelay:5.0f];
}
}
return cell;
}
- (void)addSomeMoreEntriesToTableView {
int loopTill = self.tableViewData.count + 20;
while (self.tableViewData.count < loopTill) {
[self.tableViewData addObject:[NSString stringWithFormat:#"%i", self.tableViewData.count]];
};
self.loadingMoreTableViewData = NO;
[self.tableView reloadData];
}
#end
'UITableView' is same as 'UIScrollView' in 'scrollViewDidScroll' method.
So, its easy to emulate infinite scrolling.
double the array so that head and tail are joined together to emulate circular table
use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.
:
/* To emulate infinite scrolling...
The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualising joined table in eight parts)
When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.
Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)
In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
*/
-(void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat currentOffsetX = scrollView_.contentOffset.x;
CGFloat currentOffSetY = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height;
if (currentOffSetY < (contentHeight / 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
}
if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
}
}
P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8
If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit the method when data count is say for example less than 9.
For me worked better scrollViewDidEndDragging: than scrollViewDidScroll:.
The second approach will send you each position during scroll and cause, if you are fetching remote resources you will hit your endpoint several times, which is not good.
Complete example based on #codafi solution with comments from #danielgomezrico about how to calculate contentHeight:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate {
CGFloat actualPosition = scrollView.contentOffset.y;
CGFloat contentHeight = scrollView.contentSize.height - (self.tableView.frame.size.height);
if (actualPosition >= contentHeight) {
// fetch resources
[self.tableView reloadData];
}
}
Generally I override scrollViewDidEndDecelerating and inside it I put my code to request more data.
Example:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height){
//put here your code
}
}
Recently I uploaded on GitHub a subclass of UITableView, that implements the infinite scroll.
You can download it here:
https://github.com/alchimya/iOS-LazyTableView
rather than overriding we can do this optimally in layoutSubviews.
Here's how I got it implemented. You can get to know more about the implementation here
- (void)layoutSubviews{
[super layoutSubviews];
if(self.delegateForViews){
CGPoint contentOffset = self.contentOffset;
if([self.delegateForViews noOfViews]>numOfReusableViews){
NSUInteger centerIndex=visibleViews.count/2;
NSUInteger noOfViews=[self.delegateForViews noOfViews];
UIView *centerView=[visibleViews objectAtIndex:centerIndex];
CGPoint centerViewOrigin=centerView.frame.origin;
CGSize centerViewSize=centerView.frame.size;
CGFloat offsetDifference=contentOffset.x-centerViewOrigin.x;
CGFloat offsetDifferenceAbs=fabs(contentOffset.x-centerViewOrigin.x);
if(offsetDifferenceAbs>=centerViewSize.width){
if(offsetDifference<0){
currentPosition--;
}else{
currentPosition++;
}
self.contentOffset=centerViewOrigin;
currentPosition=[self getPosition:currentPosition noOfViews:noOfViews];
[self.delegateForViews clearView:centerView];
[self.delegateForViews setupView:centerView forPosition:currentPosition];
for (int i=centerIndex-1; i>=0; i--) {
UIView* prevView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:prevView];
[self.delegateForViews setupView:prevView forPosition:
[self getPosition:currentPosition-1 noOfViews:noOfViews]];
}
for (int i=centerIndex+1; i<visibleViews.count; i++) {
UIView* nextView=[visibleViews objectAtIndex:i];
[self.delegateForViews clearView:nextView];
[self.delegateForViews setupView:nextView forPosition:
[self getPosition:currentPosition+1 noOfViews:noOfViews]];
}
}
}
}
}
One of the simple and that offered me everything i need is this class:
https://github.com/jakemarsh/JMStatefulTableViewController
You just need to subclass JMStatefulTableViewController and the it has 3 methods that you need to overwrite:
one that is called on init, to get the initial data
statefulTableViewControllerWillBeginInitialLoading
one when the user pull to refresh
statefulTableViewControllerWillBeginLoadingFromPullToRefresh
one when is called for the infinite scroll (next page)
statefulTableViewControllerWillBeginLoadingNextPage
This can be used from Cocoapods too.
scrollviewDidScroll will call when you move through the rows in tableview
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//check for the visible rows
let indexpath = self.tableView.indexPathsForVisibleRows?.last
//check if the visible row last is equal to the total number of counts
if(indexpath?.last == self.listCount){
//code for adding data to the tableview and reload the table view.
}
}
look in the link for more details about indexPathForVisibleRows
https://developer.apple.com/documentation/uikit/uitableview/1614885-indexpathsforvisiblerows

dynamicly loading content into scrollview while scrolling

I'm facing a problem with my scrollview (horizontal, paging enabled, can be 50 pages or more)
what i want to do is, load content onto the scrollView's content-View while the user is scrolling horizontally (because initially loading all the content takes way to long).
what i do is:
everytime a scrollViewDidScroll happens i check if the loading of additional content is necessary. in case it is, a new "page" is being created and added onto the scrollViews contentview at the correct position.
the problem is:
that while it preloads a page, the "scrolling-movement" gets disturbed. the screen doesn't exactly flicker, but the smoothness of the scrolling animation suffers heavily.
in code that is:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[self preLoadPageNumber:self.currentPageIndex + 2];}
-(void)preLoadPageNumber:(int)pageNumber{
NSMutableArray *tmpMovies;
VoDPage *pageView;
if(pageNumber < [self getAmountOfPages])
{
pageView = [pageViewControllers objectAtIndex:pageNumber];
if((NSNull*)pageView == [NSNull null])
{
tmpMovies = [[NSMutableArray alloc] initWithCapacity:6];
for(int o = ((pageNumber) * 6); o < ((pageNumber+1)*6); ++o)
{
#try {
[tmpMovies addObject:[movies objectAtIndex:o]];
}
#catch (NSException * e) {
break;
}
}
pageView = [[VoDPage alloc] initWithContent:tmpMovies andNavCtrl:navCtrl];
pageView.view.frame = CGRectMake(((pageNumber) * 320)-8 , 0, 320, 332);
if(editModeEnabled)
[pageView enableEditMode];
[scrollView addSubview:pageView.view];
[pageViewControllers replaceObjectAtIndex:pageNumber withObject:pageView];
}
}
}
probably I have to make some design changes here. I'm sure some of you have faced similar problems in the past.
Any help / tipps or links are greatly appreciated
thanks in advance for your help
sam
Ok i somewhat figured out a workaround that does the trick.
i changed the preLoadPageNumber- Method to look like this:
-(void)preLoadPageNumber:(NSNumber*)param
simply because i call a helper method from within ScrollViewDidScroll and said helperMethod looks like this:
-(void)performPreLoadAfterDelay
{
[self performSelector:#selector(preLoadPageNumber:) withObject:[NSNumber numberWithInt:self.currentPageIndex +1] afterDelay:0.001];
}
Calling performSelector from within the ScrollViewDidScroll Method doesn't work when scrolling very fast ("blank" pages appear)..
I don't really know why, but the trick with the helperMethod worked.
Annimation is smooth now and the "blank-page"-problem dissappeared.