无限滚动UICollectionView的两个方向与部分

tes*_*ing 12 calendar uiscrollview ios infinite-scroll uicollectionview

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

基本上,您可以检查是否到达当前滚动视图的末尾.您可以在里面scrollViewDidScroll:或里面查看collectionView:cellForItemAtIndexPath:.将另一个内容添加到数据源会很简单,但我认为还有更多内容.如果您只添加数据,则只能向下滚动.用户应该能够向两个方向滚动(向上,向下).不知道是否reloadData会这样做.也contentOffset将改变,不应该有跳跃行为.

另一种可能性是使用WWDC 2011的Advanced ScrollView Techniques中显示的方法.这里用于设置到中心,子视图的帧被调整到距离中心相同的距离.如果我没有部分,这种方法可以正常工作.如何使用部分?layoutSubviewscontentOffsetUIScrollView

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

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

编辑:

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

赏金:

现在,我正在为回答这个问题提供赏金.我对如何实现iOS日历的月视图感兴趣.详细介绍无限滚动的工作原理.它在两个方向上工作(向上,向下),它永远不会结束(真正的无限 - 不重复).也没有任何延迟(即使在iPhone 4上).我想使用UICollectionView和数据由不同的部分组成,每个部分有不同数量的项目.必须做一些计算才能得到下一部分.我不需要日历部分 - 只有部分中不同项目的无限滚动行为.随意提问.

添加部分:

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

        );
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我们接近顶部应用程序崩溃

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

我认为它崩溃了,因为它经常被调用.如果我删除contentOffset自适应它确实有效,但我总是在顶部.如果我在顶部,则会添加越来越多的部分.因此需要限制此算法.我也有一个初始内容偏移.这种偏移是错误的,因为在初始化时也会调用算法并添加一些部分.现在我尝试添加部分didEndDisplayingCell但它崩溃了.

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

Ami*_*ngh 17

经过大量的研发后,我为你找到了答案,答案是: -

RSDayFlow这是使用开发DayFlow 我已经通过大部分的部分过去了,我建议,如果你想使日历应用程序,使用DayFlow图书馆,它的好.

现在我们来谈谈他们如何管理无限流动,并相信我的朋友,我花了很长时间才明白这一点,这些人在构建这个时真的想到了!

1.)首先,他们已经开始创建一个结构,在 RSDayFlow.h

typedef struct {
    NSUInteger year;
    NSUInteger month;
    NSUInteger day;
} RSDFDatePickerDate;
Run Code Online (Sandbox Code Playgroud)

这是用于维护两个属性

@property (nonatomic, readonly, assign) RSDFDatePickerDate fromDate;
@property (nonatomic, readonly, assign) RSDFDatePickerDate toDate;
Run Code Online (Sandbox Code Playgroud)

RSDFDatePickerView这其中拥有UICollectionView视图(子类,以RSDFDatePickerCollectionView)和其他一切在屏幕上可见(除了的导航栏和课程的TabBar).RSDFDatePickerView RSDFDatePickerViewController使用与ViewController相同的视图边界进行初始化.

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

[self.datePickerView selectDate:today];
Run Code Online (Sandbox Code Playgroud)

现在,在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];
}
Run Code Online (Sandbox Code Playgroud)

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

- (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];
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,正如人们可以猜到的那样,当前部分结果为6,即月份和单元格项目号.是一天.

唷!就是这样,上面是基本概述,让我们了解无限卷轴,它来了......

2.)我们的UICollectionView的SubClass,即RSDFDatePickerCollectionView覆盖

- (void)layoutSubviews;
Run Code Online (Sandbox Code Playgroud)

UICollectionView的方法(由layoutIfNeeded自动调用).现在我们在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
Run Code Online (Sandbox Code Playgroud)

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

嘿! 你为什么不马上就到了?

嘿!  你为什么不马上就到了?

: - | 我即将到来,就在那里,好吧!

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

#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];
    }
}
Run Code Online (Sandbox Code Playgroud)

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

内心的平静 !!

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

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

- (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;
    })())];
}
Run Code Online (Sandbox Code Playgroud)

在这两个函数中你会注意到一个块被执行,它的shiftDatesByComponents,它是我的逻辑的核心,因为这个家伙真正的魔法,它有点棘手,这里是:

- (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)
    }];
}
Run Code Online (Sandbox Code Playgroud)

在几行中解释上述功能它基本上做的是,根据更新计算的范围,未来6个月的rage或过去6个月的范围,它操纵collectionView的dataSource,未来6个月不会成为问题,你只需添加东西,但过去6个月是真正的挑战.

发生了什么,

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

        }
Run Code Online (Sandbox Code Playgroud)

男人我累了!因为这个问题,我没有睡一点,做一件事,如果你有任何疑问,请打我!

PS这是唯一能让你像官方iOS日历应用程序一样平滑滚动的技术,我看到很多人操纵scrollView及其委托方法来实现无限滚动,没有看到任何平滑.问题是,操作UICollectionView Delegate如果正确完成会造成更少的伤害,因为它们是为了努力工作而做的.

在此输入图像描述

  • 嗨,我写了DayFlow。阿米特的答案看起来很合理。:) (2认同)
  • 我用来重新定位滚动视图的技巧非常简单.首先,在需要更多数据的方向上生成更多数据.如果您向上滚动,则生成较旧的数据,否则生成较新的数据.其次,一旦生成了数据,找到一个公共元素并计算偏移量增量,然后非常快速地对其进行补偿. (2认同)