且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

在 UICollectionView 的两个方向上无限滚动,带有部分

更新时间:2023-02-05 11:48:05

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.