使用部分在 UICollectionView 的两个方向上无限滚动
Infinitely scrolling in both directions of UICollectionView with sections
我有一个类似于 iOS 日历的月视图,并且使用了 UICollectionView
。现在实现无限滚动行为会很有趣,这样用户就可以在每个方向垂直滚动,而且滚动永远不会结束。现在的问题是如何以有效的方式实施这种行为?这是我现在发现的:
基本上你可以检查你是否到达了当前滚动视图的末尾。您可以在 scrollViewDidScroll:
或 collectionView:cellForItemAtIndexPath:
中查看。向数据源添加其他内容很简单,但我认为不止于此。例如,如果您只添加数据,则只能向下滚动。用户应该能够在两个方向(向上、向下)滚动。不知道 reloadData
是否可以解决问题。 contentOffset
也会改变,应该没有跳跃行为。
另一种可能性是使用 Advanced ScrollView Techniques of 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;
}
);
}
}
如果我们接近顶部,应用程序会崩溃
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
我认为它崩溃是因为它被调用得太频繁了。如果我删除 contentOffset 改编它确实有效,但我总是在最前面。如果我在顶部,则会添加越来越多的部分。所以这个算法需要限制。我还有一个初始内容偏移量。这个偏移量是错误的,因为在初始化时算法也被调用并添加了一些部分。现在我尝试在 didEndDisplayingCell
中添加部分,但它崩溃了。
在末尾添加部分确实有效,但是当我添加它时(之前的一个部分或之前的 10 个部分)并不重要。当更新发生时,滚动有一些卡顿。我尝试的另一件事是将部分的数量从 12 个减少到 3 个,但随后出现越来越多的卡顿。
创建 UITableViewController
的子类,然后在 table 单元格中添加 UICollectionView
。 Here 是执行相同操作的示例代码。
经过大量的研发,我为你想出了一个答案,答案是:-
RSDayFlow which is developed using DayFlow
我已经完成了它的大部分内容,我建议,如果你想制作日历应用程序,请使用 DayFlow 库,它很好。
现在我们来谈谈他们是如何管理无限流量的,相信我,我的朋友,我花了很长时间才理解这一点,这些人在构建这个时真的考虑周全了!
1.) 首先,他们已经开始在 RSDayFlow.h
中创建一个结构
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.
相同的视图边界
现在,顾名思义,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];
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];
}
还有一件更重要的事情,在分配当前日期即今天的日期的同时,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];
}
}
}
}
因此可以猜到,当前部分结果为 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
此委托从 CollectionView 中的 - (void)layoutSubviews;
调用,并在 RSDFDatePickerView.m
中实现
Hey! Why don't you come to the point straight away ???
:-| I am about to, just hang in there, alright!
因此,正如我所解释的,以下是 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];
}
}
Here, above is the key, to achieve inner peace :-)
正如你所看到的,逻辑,根据 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;
})())];
}
在这两个函数中,您会注意到执行了一个块,它的 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)
}];
}
用几行来解释上面的函数,它的基本作用是,根据更新计算的范围,无论是未来 6 个月的范围还是过去 6 个月的范围,它操纵 collectionView 的数据源,未来 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)
}]];
}
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。这是唯一能让你像官方 iOS 日历应用程序那样平滑滚动的技术,我看到很多人操纵 scrollView 及其委托方法来实现无限滚动,但没有看到任何平滑度。问题是,如果操作正确,操作 UICollectionView Delegate 会造成更少的伤害,因为它们是为艰苦的工作而生的。
更简单的解决方案,对我有用:
使用 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()
,就这样。
插入顶部有点棘手,因为我们需要调整内容的偏移量。计算我们在上面插入了多少content
。
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
)
}
已在StableCollectionViewLayout实施。
实现的基本原理
override open func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
super.prepare(forCollectionViewUpdates: updateItems)
// there is possible to calculate a content offset the difference
// with the help layout attributes for each updated item or only visible items
self.offset = calculate(...)
}
override open func finalizeCollectionViewUpdates() {
super.finalizeCollectionViewUpdates()
self.offset = nil
}
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
// there is necessary to add difference to/instead proposedContentOffset
if let offset = self.offset {
return offset
}
return proposedContentOffset
}
我有一个类似于 iOS 日历的月视图,并且使用了 UICollectionView
。现在实现无限滚动行为会很有趣,这样用户就可以在每个方向垂直滚动,而且滚动永远不会结束。现在的问题是如何以有效的方式实施这种行为?这是我现在发现的:
基本上你可以检查你是否到达了当前滚动视图的末尾。您可以在 scrollViewDidScroll:
或 collectionView:cellForItemAtIndexPath:
中查看。向数据源添加其他内容很简单,但我认为不止于此。例如,如果您只添加数据,则只能向下滚动。用户应该能够在两个方向(向上、向下)滚动。不知道 reloadData
是否可以解决问题。 contentOffset
也会改变,应该没有跳跃行为。
另一种可能性是使用 Advanced ScrollView Techniques of 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;
}
);
}
}
如果我们接近顶部,应用程序会崩溃
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
我认为它崩溃是因为它被调用得太频繁了。如果我删除 contentOffset 改编它确实有效,但我总是在最前面。如果我在顶部,则会添加越来越多的部分。所以这个算法需要限制。我还有一个初始内容偏移量。这个偏移量是错误的,因为在初始化时算法也被调用并添加了一些部分。现在我尝试在 didEndDisplayingCell
中添加部分,但它崩溃了。
在末尾添加部分确实有效,但是当我添加它时(之前的一个部分或之前的 10 个部分)并不重要。当更新发生时,滚动有一些卡顿。我尝试的另一件事是将部分的数量从 12 个减少到 3 个,但随后出现越来越多的卡顿。
创建 UITableViewController
的子类,然后在 table 单元格中添加 UICollectionView
。 Here 是执行相同操作的示例代码。
经过大量的研发,我为你想出了一个答案,答案是:-
RSDayFlow which is developed using DayFlow 我已经完成了它的大部分内容,我建议,如果你想制作日历应用程序,请使用 DayFlow 库,它很好。
现在我们来谈谈他们是如何管理无限流量的,相信我,我的朋友,我花了很长时间才理解这一点,这些人在构建这个时真的考虑周全了!
1.) 首先,他们已经开始在 RSDayFlow.h
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.
现在,顾名思义,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];
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];
}
还有一件更重要的事情,在分配当前日期即今天的日期的同时,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];
}
}
}
}
因此可以猜到,当前部分结果为 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
此委托从 CollectionView 中的 - (void)layoutSubviews;
调用,并在 RSDFDatePickerView.m
Hey! Why don't you come to the point straight away ???
:-| I am about to, just hang in there, alright!
因此,正如我所解释的,以下是 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];
}
}
Here, above is the key, to achieve inner peace :-)
正如你所看到的,逻辑,根据 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;
})())];
}
在这两个函数中,您会注意到执行了一个块,它的 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)
}];
}
用几行来解释上面的函数,它的基本作用是,根据更新计算的范围,无论是未来 6 个月的范围还是过去 6 个月的范围,它操纵 collectionView 的数据源,未来 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)
}]];
}
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。这是唯一能让你像官方 iOS 日历应用程序那样平滑滚动的技术,我看到很多人操纵 scrollView 及其委托方法来实现无限滚动,但没有看到任何平滑度。问题是,如果操作正确,操作 UICollectionView Delegate 会造成更少的伤害,因为它们是为艰苦的工作而生的。
更简单的解决方案,对我有用:
使用 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()
,就这样。
插入顶部有点棘手,因为我们需要调整内容的偏移量。计算我们在上面插入了多少content
。
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
)
}
已在StableCollectionViewLayout实施。
实现的基本原理
override open func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
super.prepare(forCollectionViewUpdates: updateItems)
// there is possible to calculate a content offset the difference
// with the help layout attributes for each updated item or only visible items
self.offset = calculate(...)
}
override open func finalizeCollectionViewUpdates() {
super.finalizeCollectionViewUpdates()
self.offset = nil
}
override open func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
// there is necessary to add difference to/instead proposedContentOffset
if let offset = self.offset {
return offset
}
return proposedContentOffset
}