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

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

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

另一种可能性是使用WWDC 2011的 高级滚动视图技术中显示的方法。 这里layoutSubviews用于将contentOffset设置为UIScrollView的中心,子视图的框架被调整到与中心距离相同的距离。 如果我没有部分,这种方法将工作正常。 这将如何与部分工作?

我不想使用高数值的部分来伪造无限滚动,因为用户会发现结束。 另外我不使用任何分页。

那么我怎样才能实现集合视图的无限滚动?

编辑:

现在我试图增加节的数量,如果我打到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; } ); } } 

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

对未呈现的视图进行快照会产生空的快照。 确保您的视图在屏幕更新后的快照或快照之前至less已呈现一次。 – [Procet_UICollectionViewCell _addUpdateAnimation],/SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionViewCell.m:147中的断言失败

我认为它崩溃,因为它被称为太频繁。 如果我删除contentOffset适配它确实工作,但我总是在最上面。 如果我在上面,更多的部分被添加。 所以这个algorithm需要被限制。 我也有一个初始的内容偏移。 这个偏移量是错误的,因为在初始化时,algorithm也被调用并添加了一些部分。 现在我试图添加didEndDisplayingCell的部分,但它崩溃。

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

经过大量的研发工作,我为您提供了一个答案,答案是:

使用DayFlow开发的RSDayFlow我经历了大部分的部分,我build议,如果你想制作日历应用程序,使用DayFlow库,它的好处。

现在我们来谈谈他们如何pipe理无限的stream量,并相信我的朋友,我花了相当长的一段时间才明白这一点,这些家伙真的在思考这个问题的时候考虑过了!

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

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

这是用于维护两个属性

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

RSDFDatePickerView这是保存UICollectionView(子类RSDFDatePickerCollectionView)和其他一切可见的屏幕(除了导航栏和TabBar当然)的视图。 RSDFDatePickerView是从RSDFDatePickerViewController初始化的,具有与ViewController相同的视图边界。

现在,如名称所示,从date和toDate被用作显示日历的范围。 最初这个fromDate和toDate分别计算为从当前date起的-6个月和+6个月,也就是当前date在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]; 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]; } 

现在又一件重要的事情是,在分配当前date(即今天的date)的同时,也决定了CollectionView当前单元格的indexpath,看看之前调用的函数:

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

所以我们可以猜到,当前部分是6,即月份和单元格编号。 是一天。

唷! 就是这样,上面是基本的概述,因为我们要理解无限的滚动,在这里来…

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

 - (void)layoutSubviews; 

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 

这个委托从- (void)layoutSubviews;被调用- (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]; } } 

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

内心的平静 !!

正如你所看到的那样,如果pickerCollectionView.contentOffset变成小于零,我们将继续添加6个月的过去date,如果pickerCollectionView.contentOffset变得更大,则contentSize和bounds的差异我们将继续增加6个月的未来date。

但是我的朋友生活中没有这么简单,这两个function就是一切。

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

在这两个函数中,你会注意到一个块被执行,它的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) }]; } 

为了解释上面几行中的function,它基本上是做什么的,根据更新已经计算的范围,将来6个月的风暴或过去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) }]]; } 

男人我累了! 因为这个问题,我没有睡一会儿,做一件事,如果有什么疑问的话,就ping我吧!

PS这是唯一的技术,让你顺利滚动像官方的iOS日历应用程序,我看到很多人操纵scrollView及其委托方法实现无限滚动,没有看到任何平滑。 事情是,操纵UICollectionView委托会造成更less的伤害,如果做得正确,因为他们是努力工作。

在这里输入图像说明

更简单的解决scheme,适用于我:

使用viewWillLayoutSubviews来确定何时以及如何更新模型。

 override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() let topEdge: CGFloat = 0 let bottomEdge = collectionView.contentSize.height - collectionView.bounds.height if collectionView.contentOffset.y < topEdge { insertTop() } else if collectionView.contentOffset.y > bottomEdge { insertBottom() } } 

追加到底部通常很容易,只需将数据追加到模型中,然后在集合视图上调用reloadData() ,就是这样。

插入顶部有点棘手,因为我们需要调整内容的偏移量。 计算我们在顶部插入了多lesscontent

 func insertTop { let beforeSize = collectionView.collectionViewLayout.collectionViewContentSize // insert data at the beginning of your model // ... collectionView.reloadData() let afterSize = collectionView.collectionViewLayout.collectionViewContentSize let diff = afterSize.height - beforeSize.height collectionView.contentOffset = CGPoint( x: collectionView.contentOffset.x, y: collectionView.contentOffset.y + diff ) } 

创buildUITableViewController的子类,然后在表格单元格中添加UICollectionView 。 这是一个示例代码,它也是这样做的。