Бесконечная прокрутка в обоих направлениях UICollectionView с разделами

у меня есть месяц, похожий на календарь iOS и есть. Теперь было бы интересно реализовать бесконечное поведение прокрутки, чтобы пользователь мог прокручивать в каждом направлении вертикально, и это никогда не закончится. Теперь вопрос в том, как такое поведение может быть эффективно реализовано? Вот что я теперь выяснил:--14-->

в основном вы можете проверить, попали ли вы в конец текущего вида прокрутки. Вы можете проверить это в scrollViewDidScroll: или collectionView:cellForItemAtIndexPath:. Это просто добавьте другой контент в источник данных, но я думаю, что есть больше. Если вы только добавляете данные, вы можете прокручивать только вниз, например. Пользователь должен иметь возможность перемещаться в обоих направлениях (вверх, вниз). Не знаю, если reloadData будет делать трюк. Также contentOffset изменится и не должно быть никаких прыжков поведения.

другой возможностью было бы использовать подход, показанный в Расширенные Методы ScrollView of конференции WWDC . Вот!--6--> используется для задания contentOffset в центр UIScrollView и рамки подвидов настраиваются на одинаковое расстояние от центра. Этот подход будет работать нормально, если у меня нет разделов. Как это будет работать с разделами?

Я не хочу использовать высокое значение для количества разделов, чтобы подделать бесконечный свиток, потому что пользователь найдет конец. Кроме того, я не использую пейджинг.

Итак, как я могу реализовать бесконечную прокрутку для представления коллекции?

Edit:

теперь я попытался увеличить количество разделов, если я попал в конец 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 представление, которое не было отображено, приводит к пустому снимок. Убедитесь, что ваше представление было отображено хотя бы один раз снимки или снимок после обновления экрана. Ошибка утверждения в - [Procet_UICollectionViewCell _addUpdateAnimation], / SourceCache/UIKit_Sim/UIKit-2935.137 / UICollectionViewCell.m: 147

Я думаю, что он падает, потому что он называется слишком часто. Если я удалю contentOffset адаптация это работает, но я всегда на высоте. Если я на вершине ... все больше и больше разделов добавляются. Поэтому этот алгоритм должен быть ограничен. У меня также есть начальное смещение контента. Это смещение неверно, потому что при инициализации алгоритм также вызывается и добавляет некоторые разделы. Теперь я попытался добавить разделы в didEndDisplayingCell но она падает.

добавление разделов в конце действительно работает, но не имеет значения, когда я добавляю его (один раздел до или 10 разделов до). Когда обновление происходит, прокрутка имеет некоторое заикание. Еще одна вещь, которую я пробовал: чтобы уменьшить количество секций с 12 до 3, но затем все больше и больше заикаются.

3 ответов


после многих R&D Я придумал ответ для вас, и ответ: -

RSDayFlow который разработан с использованием 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 ) и все остальное, видимое на экране (кроме navigationBar и TabBar of-course). 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];
}

а теперь еще одна важная вещь, назначая текущую дату, т. е. сегодняшнюю дату, также определяется indexpath текущего элемента ячейки 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

этот делегат вызывается из - (void)layoutSubviews; в CollectionView и его реализация в RSDFDatePickerView.m

Эй! Почему бы тебе не перейти к делу? прямо сейчас ???

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

:-| я собираюсь, просто держись, ладно!

Итак, как я объяснял, ниже приведена реализация RSDFDatePickerCollectionViewDelegate в RSDFDatePickerView.м

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

вот, выше ключ, чтобы достичь внутреннего мира: -)

Inner Peace !!

как вы можете видеть, логика, говоря в терминах y-компонента т. е. высота, если pickerCollectionView.contentOffset становится меньше нуля мы будем продолжать добавлять прошлые даты на 6 месяцев, и если pickerCollectionView.contentOffset становится больше, чем разница contentSize и границ мы будем продолжать добавлять будущие даты на 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)
            }]];

        }

человек я устал! Я не сплю из-за этой проблемы, сделать одну вещь, если у вас есть какие-либо сомнения, пинг меня!

P.S. Это единственный метод, который дает вам плавную прокрутку, как официальное приложение календаря iOS, я видел, что многие люди манипулируют scrollView и его методом делегата для достижения бесконечной прокрутки, не видели там никакой гладкости. Дело в том, что манипулирование делегатом UICollectionView вызовет меньше вред, если сделано правильно, потому что они сделаны для тяжелой работы.

enter image description here


более простое решение, которое работает для меня:

использовать 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
    )
}

создать UITableViewController's подкласс, а затем добавить UICollectionView в ячейке таблицы. здесь пример кода, который делает то же самое.