在 UICollectionView 的两个方向上无限滚动,带有部分 [英] Infinitely scrolling in both directions of UICollectionView with sections

查看:21
本文介绍了在 UICollectionView 的两个方向上无限滚动,带有部分的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个类似于 iOS 日历的月视图,并且使用了 UICollectionView.现在实现无限滚动行为会很有趣,这样用户可以在每个方向上垂直滚动并且永远不会结束.现在的问题是如何以有效的方式实施这种行为?这是我现在发现的:

基本上,您可以检查是否到达当前滚动视图的末尾.您可以在 scrollViewDidScroll:collectionView:cellForItemAtIndexPath: 中检查这一点.向数据源添加其他内容很简单,但我认为不止这些.例如,如果您只添加数据,则只能向下滚动.用户应该能够在两个方向(向上、向下)滚动.不知道 reloadData 是否可以解决问题.contentOffset 也会发生变化,应该没有跳跃行为.

另一种可能性是使用 WWDC 2011 的 DTS40011102-Intro-DontLinkElementID_2">高级 ScrollView 技术.这里 layoutSubviews 用于将 contentOffset 设置为 UIScrollView 的中心,并将子视图的帧调整为相同的距离从中心.如果我没有部分,这种方法会很好用.这将如何与部分一起使用?

我不想使用较高的部分数来伪造无限滚动,因为用户会找到结尾.我也不使用任何分页.

那么如何实现集合视图的无限滚动呢?

现在,如果我到达 UICollectionView 的末尾,我会尝试增加部分的数量.要显示新部分,必须调用 reloadData.在调用此方法时,所有当前可用部分的所有计算都将再次完成!这个性能问题会导致在集合视图中滚动时出现严重的卡顿,并且如果向下滚动,它会变得越来越慢.不知道是否可以在后台线程上转移这项工作.通过这种方法,如果您进行了必要的调整,您可以向上和向下滚动.

赏金:

现在我悬赏回答这个问题.我对如何实现 iOS 日历的月视图很感兴趣.详细说明无限滚动是如何工作的.在这里,它在两个方向(向上,向下)都有效,并且永远不会结束(真正的无限 - 没有重复).也完全没有延迟(即使在 iPhone 4 上).我想使用 UICollectionView 并且数据由不同的部分组成,每个部分都有不同数量的项目.必须进行一些计算才能获得下一部分.我不需要日历部分——只需要一个部分中不同项目的无限滚动行为.欢迎提问.

添加部分:

public override void Scrolled(UIScrollView scrollView){NSIndexPath[] currentIndexPaths = currentVisibleIndexPaths();//如果我们在顶部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(委托(){//一些日历计算和更新这里没有显示的数据源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 (currentIndexPaths.Last().Section == this.controller.CurrentNumberOfSections - 1){this.controller.CollectionView.PerformBatchUpdatesAsync(委托(){//一些日历计算和更新这里没有显示的数据源this.controller.CollectionView.InsertSections(NSIndexSet.FromNSRange(new NSRange(this.controller.CurrentNumberOfSections, 12)));this.controller.CurrentNumberOfSections += 12;});}}

如果我们接近顶部,应用会崩溃

<块引用>

对尚未渲染的视图进行快照会导致空白快照.确保您的视图之前至少渲染过一次屏幕更新后的快照或快照.断言失败-[Procet_UICollectionViewCell _addUpdateAnimation],/SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionViewCell.m:147

我认为它崩溃是因为它被频繁调用.如果我删除 contentOffset 改编它确实有效,但我总是在最上面.如果我在上面,就会添加越来越多的部分.所以这个算法需要被限制.我也有一个初始内容偏移量.这个偏移量是错误的,因为在初始化时该算法也被调用并添加了一些部分.现在我尝试在 didEndDisplayingCell 中添加这些部分,但它崩溃了.

在末尾添加部分确实有效,但我添加它时并不重要(之前的一个部分或之前的 10 个部分).当更新发生时,滚动会有些卡顿.我尝试的另一件事是将部分的数量从 12 减少到 3,但随后出现越来越多的卡顿.

解决方案

经过大量的研发,我想出了一个答案给你,答案是:-

RSDayFlow 使用 DayFlow我已经完成了大部分内容,我建议,如果你想制作日历应用程序,请使用 DayFlow 库,它的不错.

现在我们来谈谈他们是如何管理无限流的,相信我,我的朋友,我花了很长时间才明白这一点,这些家伙在构建这个时真的考虑过了!

1.) 首先,他们开始在 RSDayFlow.h

中创建一个结构体

typedef 结构 {NSU整数年;NSU整数月;NSU整数天;} RSDFDatePickerDate;

这是用来维护两个属性的

@property (nonatomic, readonly, assign) RSDFDatePickerDate fromDate;@property (nonatomic, readonly, assign) RSDFDatePickerDate toDate;

RSDFDatePickerView 中,该视图包含 UICollectionView(子类为 RSDFDatePickerCollectionView)和屏幕上可见的所有其他内容(当然,除了 navigationBar 和 TabBar).RSDFDatePickerView 是从 RSDFDatePickerViewController 初始化的,具有与 ViewController 相同的视图边界.

现在,顾名思义,fromDate 和 toDate 用作显示日历的范围.最初,fromDate 和 toDate 分别计算为从当前日期开始的 -6 个月和 +6 个月,当前日期也在 RSDFDatePickerViewController 中设置,它自己调用以下方法:

[self.datePickerView selectDate:today];

现在在 RSDFDatePickerView 中调用以下方法

- (void)commonInitializer{NSDateComponents *nowYearMonthComponents = [self.calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth) fromDate:[NSDate date]];NSDate *now = [self.calendar dateFromComponents:nowYearMonthComponents];_fromDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:((^{NSDateComponents *components = [NSDateComponents new];组件.月 = -6;返回组件;})()) toDate:now options:0]];_toDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:((^{NSDateComponents *components = [NSDateComponents new];组件.月 = 6;返回组件;})()) toDate:now options:0]];NSDateComponents *todayYearMonthDayComponents = [self.calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay) fromDate:[NSDate date]];_today = [self.calendar dateFromComponents:todayYearMonthDayComponents];[[NSNotificationCenter defaultCenter] addObserver:self选择器:@selector(significantTimeChange:)名称:UIApplicationSignificantTimeChangeNotification对象:无];}

现在还有一件更重要的事情,在分配当前日期即今天的日期的同时,CollectionView的当前单元格项的索引路径也确定了,看看之前调用的函数:

- (void)selectDate:(NSDate *)date{if (![self.selectedDate isEqual:date]) {if (self.selectedDate &&[self.selectedDate 比较:[self dateFromPickerDate:self.fromDate]] != NSOrderedAscending &&[self.selectedDate 比较:[self dateFromPickerDate:self.toDate]] != NSOrderedDescending) {NSIndexPath *previousSelectedCellIndexPath = [self indexPathForDate:self.selectedDate];[self.collectionView deselectItemAtIndexPath:previousSelectedCellIndexPath Animation:NO];UICollectionViewCell *previousSelectedCell = [self.collectionView cellForItemAtIndexPath:previousSelectedCellIndexPath];if (previousSelectedCell) {[previousSelectedCell setNeedsDisplay];}}_selectedDate = 日期;if (self.selectedDate &&[self.selectedDate 比较:[self dateFromPickerDate:self.fromDate]] != NSOrderedAscending &&[self.selectedDate 比较:[self dateFromPickerDate:self.toDate]] != NSOrderedDescending) {NSIndexPath *indexPathForSelectedDate = [self indexPathForDate:self.selectedDate];[self.collectionView selectItemAtIndexPath:indexPathForSelectedDate 动画:NO scrollPosition:UICollectionViewScrollPositionNone];UICollectionViewCell *selectedCell = [self.collectionView cellForItemAtIndexPath:indexPathForSelectedDate];如果(选定单元格){[selectedCell setNeedsDisplay];}}}}

所以可以猜到,当前部分结果是 6,即月份和单元格项目编号.是这一天.

呸!就是这样,上面是基本的概述,为了我们理解无限滚动,来了……

2.) 我们的 UICollectionView 子类,即 RSDFDatePickerCollectionView 覆盖了

- (void)layoutSubviews;

UICollectionView 的方法(由 layoutIfNeeded 自动调用).现在我们在 RSDFDatePickerCollectionView 中定义了一个协议.

@protocol RSDFDatePickerCollectionViewDelegate <UICollectionViewDelegate>///------------------------------------///@name 支持布局子视图///------------------------------------/**告诉委托集合视图将布局子视图.@param pickerCollectionView 将布局子视图的集合视图.*/- (void)pickerCollectionViewWillLayoutSubviews:(RSDFDatePickerCollectionView *)pickerCollectionView;@结尾

此委托是从 CollectionView 中的 - (void)layoutSubviews; 调用的,并在 RSDFDatePickerView.m

中实现<块引用>

嘿!为什么不直接说重点???

<块引用>

:-|我要了,就等着吧,好吧!

所以,正如我所解释的,以下是 RSDFDatePickerView.m 中 RSDFDatePickerCollectionViewDelegate 的实现

#pragma 标记 - RSDFDatePickerCollectionViewDelegate- (void)pickerCollectionViewWillLayoutSubviews:(RSDFDatePickerCollectionView *)pickerCollectionView{//注意:重新布局比一次计算 3 或 6 个月的数据要慢//所以我们一次下注 6 个月.//运行时间自身符号名称////1647.0ms 23.7% 1647.0 objc_msgSend//193.0ms 2.7% 193.0 -[NSIndexPath compare:]//163.0ms 2.3% 163.0 objc::DenseMap, objc::DenseMapInfo>::LookupBucketFor(objc_object* const&, std::pair<objc_object*, unsigned long>*&) const//141.0ms 2.0% 141.0 DYLD-STUB$$-[_UIHostedTextServiceSessiondismissTextServiceAnimated:]//138.0ms 1.9% 138.0 -[NSObject 保留]//136.0ms 1.9% 136.0 -[NSIndexPath indexAtPosition:]//124.0ms 1.7% 124.0 -[_UICollectionViewItemKey isEqual:]//118.0ms 1.7% 118.0 _objc_rootReleaseWasZero//105.0ms 1.5% 105.0 DYLD-STUB$$CFDictionarySetValue$shimif (pickerCollectionView.contentOffset.y < 0.0f) {[自我追加过去日期];}if (pickerCollectionView.contentOffset.y > (pickerCollectionView.contentSize.height - CGRectGetHeight(pickerCollectionView.bounds))) {[自我追加未来日期];}}

<块引用>

这里,上面是实现内心平静的关键:-)

正如你所看到的,逻辑,在 y 分量(即高度)方面,如果 pickerCollectionView.contentOffset 变得小于零,我们将继续添加过去的日期 6 个月,如果 pickerCollectionView.contentOffset 变得更大,那么差异contentSize 和 bounds 我们将在 6 个月后继续添加未来的日期.

但是我的朋友,生活中没有什么比这更容易的了,这两个功能就是一切..

- (void)appendPastDates{[自我 shiftDatesByComponents:((^{NSDateComponents *dateComponents = [NSDateComponents new];dateComponents.month = -6;返回日期组件;})())];}- (void)appendFutureDates{[自我 shiftDatesByComponents:((^{NSDateComponents *dateComponents = [NSDateComponents new];dateComponents.month = 6;返回日期组件;})())];}

在这两个函数中,您会注意到执行了一个块,它的 shiftDatesByComponents,据我所知,它是逻辑的核心,因为这家伙真的很神奇,它有点棘手,这里是:

- (void)shiftDatesByComponents:(NSDateComponents *)components{RSDFDatePickerCollectionView *cv = self.collectionView;RSDFDatePickerCollectionViewLayout *cvLayout = (RSDFDatePickerCollectionViewLayout *)self.collectionView.collectionViewLayout;NSArray *visibleCells = [cv visibleCells];if (![visibleCells count])返回;NSIndexPath *fromIndexPath = [cv indexPathForCell:((UICollectionViewCell *)visibleCells[0]) ];NSInteger fromSection = fromIndexPath.section;NSDate *fromSectionOfDate = [self dateForFirstDayInSection:fromSection];UICollectionViewLayoutAttributes *fromAttrs = [cvLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:fromSection]];CGPoint fromSectionOrigin = [self convertPoint:fromAttrs.frame.origin fromView:cv];_fromDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:components toDate:[self dateFromPickerDate:self.fromDate] options:0]];_toDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:components toDate:[self dateFromPickerDate:self.toDate] options:0]];#如果为0//这个解决方案会使集合视图有点跳闸//因为我们的重新加载是反动的,并且发生在重新布局之前//因为我们必须这样做以避免闪烁和质问 CA 事务 (?)//这也可能是一个小红旗[cv performBatchUpdates:^{if (components.month < 0) {[cv deleteSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){cv.numberOfSections - abs(components.month),绝对(组件.月)}]];[cv insertSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){0,绝对(组件.月)}]];} 别的 {[cv insertSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){cv.numberOfSections,绝对(组件.月)}]];[cv deleteSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){0,绝对(组件.月)}]];}}完成:^(布尔完成){NSLog(@"%s %x", __PRETTY_FUNCTION__, 完成);}];for (UIView *view in cv.subviews)[view.layer removeAllAnimations];#别的[简历重载数据];[cvLayout 无效布局];[cvLayout 准备布局];[自恢复选择];#万一NSInteger toSection = [self sectionForDate:fromSectionOfDate];UICollectionViewLayoutAttributes *toAttrs = [cvLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:toSection]];CGPoint toSectionOrigin = [self convertPoint:toAttrs.frame.origin fromView:cv];[cv setContentOffset:(CGPoint) {cv.contentOffset.x,cv.contentOffset.y + (toSectionOrigin.y - fromSectionOrigin.y)}];}

用几行来解释上面的函数它的基本作用是,根据更新计算出的范围,无论是未来 6 个月的范围还是过去 6 个月的范围,它都会操纵 collectionView 的数据源,未来 6 个月不会是个问题,你只需要添加一些东西,但过去 6 个月才是真正的挑战.

发生了什么,

if (components.month < 0) {[cv deleteSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){cv.numberOfSections - abs(components.month),绝对(组件.月)}]];[cv insertSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){0,绝对(组件.月)}]];}

<块引用>

我累了!因为这个问题我没睡好,做一件事,有什么疑问,ping我!

附:这是唯一一种可以像 iOS 官方日历应用程序那样为您提供平滑滚动的技术,我看到很多人操纵 scrollView 及其委托方法来实现无限滚动,但没有看到任何平滑度.问题是,如果操作正确,操作 UICollectionView 委托会造成更少的伤害,因为它们是为辛勤工作而设计的.

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.

解决方案

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

typedef struct {
    NSUInteger year;
    NSUInteger month;
    NSUInteger day;
} RSDFDatePickerDate;

this is the used for maintaining two properties

@property (nonatomic, readonly, assign) RSDFDatePickerDate fromDate;
@property (nonatomic, readonly, assign) RSDFDatePickerDate toDate;

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 from RSDFDatePickerViewController 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:

[self.datePickerView selectDate:today];

Now while initialising following method is called in the RSDFDatePickerView

- (void)commonInitializer
{
    NSDateComponents *nowYearMonthComponents = [self.calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth) fromDate:[NSDate date]];
    NSDate *now = [self.calendar dateFromComponents:nowYearMonthComponents];

    _fromDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:((^{
        NSDateComponents *components = [NSDateComponents new];
        components.month = -6;
        return components;
    })()) toDate:now options:0]];

    _toDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:((^{
        NSDateComponents *components = [NSDateComponents new];
        components.month = 6;
        return components;
    })()) toDate:now options:0]];

    NSDateComponents *todayYearMonthDayComponents = [self.calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay) fromDate:[NSDate date]];
    _today = [self.calendar dateFromComponents:todayYearMonthDayComponents];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(significantTimeChange:)
                                                 name:UIApplicationSignificantTimeChangeNotification
                                               object:nil];
}

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:

- (void)selectDate:(NSDate *)date
{
    if (![self.selectedDate isEqual:date]) {
        if (self.selectedDate &&
            [self.selectedDate compare:[self dateFromPickerDate:self.fromDate]] != NSOrderedAscending &&
            [self.selectedDate compare:[self dateFromPickerDate:self.toDate]] != NSOrderedDescending) {
            NSIndexPath *previousSelectedCellIndexPath = [self indexPathForDate:self.selectedDate];
            [self.collectionView deselectItemAtIndexPath:previousSelectedCellIndexPath animated:NO];
            UICollectionViewCell *previousSelectedCell = [self.collectionView cellForItemAtIndexPath:previousSelectedCellIndexPath];
            if (previousSelectedCell) {
                [previousSelectedCell setNeedsDisplay];
            }
        }

        _selectedDate = date;

        if (self.selectedDate &&
            [self.selectedDate compare:[self dateFromPickerDate:self.fromDate]] != NSOrderedAscending &&
            [self.selectedDate compare:[self dateFromPickerDate:self.toDate]] != NSOrderedDescending) {
            NSIndexPath *indexPathForSelectedDate = [self indexPathForDate:self.selectedDate];
            [self.collectionView selectItemAtIndexPath:indexPathForSelectedDate animated:NO scrollPosition:UICollectionViewScrollPositionNone];
            UICollectionViewCell *selectedCell = [self.collectionView cellForItemAtIndexPath:indexPathForSelectedDate];
            if (selectedCell) {
                [selectedCell setNeedsDisplay];
            }
        }
    }
}

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

- (void)layoutSubviews;

method of the UICollectionView (called by layoutIfNeeded automatically). Now we have a protocol defined in our RSDFDatePickerCollectionView.

@protocol RSDFDatePickerCollectionViewDelegate <UICollectionViewDelegate>

///---------------------------------
/// @name Supporting Layout Subviews
///---------------------------------

/**
 Tells the delegate that the collection view will layout subviews.

 @param pickerCollectionView The collection view which will layout subviews.
 */
- (void) pickerCollectionViewWillLayoutSubviews:(RSDFDatePickerCollectionView *)pickerCollectionView;

@end

this delegate is called from the - (void)layoutSubviews; in CollectionView and its been implemented in RSDFDatePickerView.m

Hey! Why don't you come to the point straight away ???

:-| I am about to, just hang in there, alright!

So, as I was explaining, following is the implementation of the RSDFDatePickerCollectionViewDelegate in RSDFDatePickerView.m

#pragma mark - RSDFDatePickerCollectionViewDelegate

- (void)pickerCollectionViewWillLayoutSubviews:(RSDFDatePickerCollectionView *)pickerCollectionView
{
    //  Note: relayout is slower than calculating 3 or 6 months’ worth of data at a time
    //  So we punt 6 months at a time.

    //  Running Time    Self        Symbol Name
    //
    //  1647.0ms   23.7%    1647.0      objc_msgSend
    //  193.0ms    2.7% 193.0       -[NSIndexPath compare:]
    //  163.0ms    2.3% 163.0       objc::DenseMap<objc_object*, unsigned long, true, objc::DenseMapInfo<objc_object*>, objc::DenseMapInfo<unsigned long> >::LookupBucketFor(objc_object* const&, std::pair<objc_object*, unsigned long>*&) const
    //  141.0ms    2.0% 141.0       DYLD-STUB$$-[_UIHostedTextServiceSession dismissTextServiceAnimated:]
    //  138.0ms    1.9% 138.0       -[NSObject retain]
    //  136.0ms    1.9% 136.0       -[NSIndexPath indexAtPosition:]
    //  124.0ms    1.7% 124.0       -[_UICollectionViewItemKey isEqual:]
    //  118.0ms    1.7% 118.0       _objc_rootReleaseWasZero
    //  105.0ms    1.5% 105.0       DYLD-STUB$$CFDictionarySetValue$shim

    if (pickerCollectionView.contentOffset.y < 0.0f) {
        [self appendPastDates];
    }

    if (pickerCollectionView.contentOffset.y > (pickerCollectionView.contentSize.height - CGRectGetHeight(pickerCollectionView.bounds))) {
        [self appendFutureDates];
    }
}

Here, above is the key, to achieve inner peace :-)

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..

- (void)appendPastDates
{
    [self shiftDatesByComponents:((^{
        NSDateComponents *dateComponents = [NSDateComponents new];
        dateComponents.month = -6;
        return dateComponents;
    })())];
}

- (void)appendFutureDates
{
    [self shiftDatesByComponents:((^{
        NSDateComponents *dateComponents = [NSDateComponents new];
        dateComponents.month = 6;
        return dateComponents;
    })())];
}

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 :

- (void)shiftDatesByComponents:(NSDateComponents *)components
{
    RSDFDatePickerCollectionView *cv = self.collectionView;
    RSDFDatePickerCollectionViewLayout *cvLayout = (RSDFDatePickerCollectionViewLayout *)self.collectionView.collectionViewLayout;

    NSArray *visibleCells = [cv visibleCells];
    if (![visibleCells count])
        return;

    NSIndexPath *fromIndexPath = [cv indexPathForCell:((UICollectionViewCell *)visibleCells[0]) ];
    NSInteger fromSection = fromIndexPath.section;
    NSDate *fromSectionOfDate = [self dateForFirstDayInSection:fromSection];
    UICollectionViewLayoutAttributes *fromAttrs = [cvLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:fromSection]];
    CGPoint fromSectionOrigin = [self convertPoint:fromAttrs.frame.origin fromView:cv];

    _fromDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:components toDate:[self dateFromPickerDate:self.fromDate] options:0]];
    _toDate = [self pickerDateFromDate:[self.calendar dateByAddingComponents:components toDate:[self dateFromPickerDate:self.toDate] options:0]];

#if 0

    //  This solution trips up the collection view a bit
    //  because our reload is reactionary, and happens before a relayout
    //  since we must do it to avoid flickering and to heckle the CA transaction (?)
    //  that could be a small red flag too

    [cv performBatchUpdates:^{

        if (components.month < 0) {

            [cv deleteSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
                cv.numberOfSections - abs(components.month),
                abs(components.month)
            }]];

            [cv insertSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
                0,
                abs(components.month)
            }]];

        } else {

            [cv insertSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
                cv.numberOfSections,
                abs(components.month)
            }]];

            [cv deleteSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
                0,
                abs(components.month)
            }]];

        }

    } completion:^(BOOL finished) {

        NSLog(@"%s %x", __PRETTY_FUNCTION__, finished);

    }];

    for (UIView *view in cv.subviews)
        [view.layer removeAllAnimations];

#else

    [cv reloadData];
    [cvLayout invalidateLayout];
    [cvLayout prepareLayout];

    [self restoreSelection];

#endif

    NSInteger toSection = [self sectionForDate:fromSectionOfDate];
    UICollectionViewLayoutAttributes *toAttrs = [cvLayout layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:toSection]];
    CGPoint toSectionOrigin = [self convertPoint:toAttrs.frame.origin fromView:cv];

    [cv setContentOffset:(CGPoint) {
        cv.contentOffset.x,
        cv.contentOffset.y + (toSectionOrigin.y - fromSectionOrigin.y)
    }];
}

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,

if (components.month < 0) {

            [cv deleteSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
                cv.numberOfSections - abs(components.month),
                abs(components.month)
            }]];

            [cv insertSections:[NSIndexSet indexSetWithIndexesInRange:(NSRange){
                0,
                abs(components.month)
            }]];

        }

Man I am tired! I didn't sleep a bit because of this problem, do one thing, if you have any doubt, ping me!

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.

这篇关于在 UICollectionView 的两个方向上无限滚动,带有部分的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆