I have a month view similar to the iOS calendar and an UICollectionView
is used. Now it would be interesting to implement an infinite scrolling behavior so that the user can scroll in each direction vertically and it will never end. The question now is how can such a behavior be implemented in an efficient way? This is what I've found out now:
Basically you can check if you hit the end of the current scroll view. You can check this in scrollViewDidScroll:
or in collectionView:cellForItemAtIndexPath:
. It would be simple to add another content to the datasource, but I think there is more than that. If you only add data you could only scroll downwards for example. The user should be able to scroll in both directions (upwards, downwards). Don't know if reloadData
would do the trick. Also the contentOffset
would change and there should be no jumping behavior.
Another possibility would be to use the approach shown in Advanced ScrollView Techniques of WWDC 2011. Here layoutSubviews
is used to set the contentOffset
to the center of the UIScrollView
and the frames of the subviews are adjusted to the same amount of the distance from the center. This approach would work fine if I have no sections. How would this work with sections?
I don't want to use a high value for the number of sections to fake a infinite scroll, because user will find the end. Also I don't use any paging.
So how can I implement infinite scrolling for the collection view?
Edit:
Now I tried to increase the number of section if I hit the end of the UICollectionView
. To show the new sections one has to call reloadData
. On calling this method all calculations for all current available sections are done again! This performance issue is causing big stutters when scrolling through the collection view and it gets slower and slower if you scroll down. Don't know if one could transfer this work on a background thread. With this approach one could scroll upwards and downwards if you make the needed adaptions.
Bounty:
Now I'm offering a bounty for answering this question. I'm interested in how the month view of the iOS calendar is implemented. In detail how does the infinite scrolling works. Here it works in both directions (upwards, downwards) and it never ends (real infinite - no repeating). Also there is no lag at all (even on an iPhone 4). I want to use the UICollectionView
and the data consists of different sections and each section has a different number of items. One has to do some calculations to get the next section. I don't need the calendar part - only the infinite scrolling behavior with the different items in a section. Feel free to ask question.
Adding Sections:
public override void Scrolled(UIScrollView scrollView)
{
NSIndexPath[] currentIndexPaths = currentVisibleIndexPaths();
// if we are at the top
if (currentIndexPaths.First().Section == 0)
{
NSIndexPath oldIndexPath = NSIndexPath.FromItemSection(0, 0);
UICollectionViewLayoutAttributes attributes_before = this.controller.CollectionView.GetLayoutAttributesForItem(oldIndexPath);
CGRect before = attributes_before.Frame;
CGPoint contentOffset = this.controller.CollectionView.ContentOffset;
this.controller.CollectionView.PerformBatchUpdatesAsync(delegate ()
{
// some calendar calculations and updating the data source not shown here
this.controller.CurrentNumberOfSections += 12;
this.controller.CollectionView.InsertSections(NSIndexSet.FromNSRange(new NSRange(0, 12)));
}
);
NSIndexPath newIndexPath = NSIndexPath.FromItemSection(0, 12);
UICollectionViewLayoutAttributes attributes_after = this.controller.CollectionView.GetLayoutAttributesForItem(newIndexPath);
CGRect after = attributes_after.Frame;
contentOffset.Y += (after.Y - before.Y);
this.controller.CollectionView.SetContentOffset(contentOffset, false);
}
// if we are near the end
if (currentIndexPaths.Last().Section == this.controller.CurrentNumberOfSections - 1)
{
this.controller.CollectionView.PerformBatchUpdatesAsync(delegate ()
{
// some calendar calculations and updating the data source not shown here
this.controller.CollectionView.InsertSections(NSIndexSet.FromNSRange(new NSRange(this.controller.CurrentNumberOfSections, 12)));
this.controller.CurrentNumberOfSections += 12;
}
);
}
}
If we are near the top the app crashes with
Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has been rendered at least once before snapshotting or snapshot after screen updates. Assertion failure in -[Procet_UICollectionViewCell _addUpdateAnimation], /SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionViewCell.m:147
I think it crashes because it is called too often. If I remove the contentOffset adaptions it does work, but I'm always on top. If I'm on top more and more sections are added. So this algorithm needs to be restricted. I also have an initial content offset. This offset is wrong because on initialization the algorithm is also called and adds some sections. Now I tried to add the sections in didEndDisplayingCell
but it crashes.
Adding sections at the end does work, but it doesn't matter when I add it (one section before or 10 sections before). When the update takes place the scrolling has some stutter. Another thing I tried was to decrease the number of sections from 12 to 3, but then more and more stutter occur.
Create
UITableViewController
's subclass and then addUICollectionView
in the table cell. Here is a sample code which does the same.More simple solution, which works for me:
Use
viewWillLayoutSubviews
to determine when and how updated your model.Append to bottom is usually easy, just append data into you model and call
reloadData()
on collection view, that's it.Insert into top is a little bit tricky because we need to adjust content's offset. Calculate how much
content
we inserted on top.After a lot of R&D I have come up with an answer for you, and the answer is :-
RSDayFlow which is developed using DayFlow I have gone through most of the part of it and I recommend, if you want to make calendar app, use the DayFlow Library, its good.
Now we come to the part as to how they have managed the infinite flow, and trust me my friend, it took me quite a while to understand this, these guys had really thought it through while building this!
1.) Firstly, they have started with creating a struct, in
RSDayFlow.h
this is the used for maintaining two properties
in
RSDFDatePickerView
which is the view which holds UICollectionView ( subclassed to RSDFDatePickerCollectionView ) and everything else visible on the screen ( Apart from the navigationBar and TabBar of-course). RSDFDatePickerView is initialised fromRSDFDatePickerViewController
with same view bounds as that of the ViewController.Now, as name suggest, fromDate and toDate is used as a range to display the calendar. Initially this fromDate and toDate is calculated as -6 months and +6 months from the current date respectively, also the current date is set in the RSDFDatePickerViewController it self calling the following method:
Now while initialising following method is called in the RSDFDatePickerView
And now again one more important thing, while assigning the current date i.e. today's date, the indexpath of the current cell item of the CollectionView is also decided, have a look at the function called previously:
So as one can guess, the current section turns out to be 6 i.e. the Month and cell item no. is the day.
Phew! that's it, above was the basic overview, for us to understand the infinite scroll, here it comes...
2.) Our SubClass of UICollectionView i.e. RSDFDatePickerCollectionView Overrides the
method of the UICollectionView (called by layoutIfNeeded automatically). Now we have a protocol defined in our RSDFDatePickerCollectionView.
this delegate is called from the
- (void)layoutSubviews;
in CollectionView and its been implemented inRSDFDatePickerView.m
So, as I was explaining, following is the implementation of the RSDFDatePickerCollectionViewDelegate in RSDFDatePickerView.m
As you can see, the logic, talking in terms of y-component i.e. height, if pickerCollectionView.contentOffset becomes less then zero we will keep adding past dates by 6 months and if the pickerCollectionView.contentOffset becomes greater then the difference of contentSize and bounds we will keep adding future dates by 6 months.
But nothing comes this easy in life my friend, These two functions is everything..
In these two function you will notice a block is performed, its shiftDatesByComponents, its the heart of the logic according to me, coz this guy does the real magic, its bit tricky, here it is :
To explain the above function in few lines what it basically does is, depending update what range has been calculated, be it future 6 month rage or past 6 month range, it manipulates the dataSource of the collectionView, future 6 months will not be a problem, you will just have to add stuff, but past 6 months is the real challenge.
Here what happens,
P.S. This is the only technique which gives you smooth scrolling like the Official iOS Calendar App, I saw many people manipulating the scrollView and its delegate method to achieve infinite scrolling, didn't see any smoothness there. The thing is, manipulating the UICollectionView Delegate will cause less harm if done correctly, coz they are made for hard work.