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

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

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

Другой возможностью было бы использовать подход Advanced ScrollView Techniques WWDC 2011. Здесь layoutSubviews используется, чтобы установить contentOffset в центр UIScrollView, и кадры подсмотров настроены на ту же величину расстояния от центра. Этот подход будет работать нормально, если у меня нет разделов. Как это будет работать с разделами?

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

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

Edit:

Теперь я попытался увеличить число секций, если попал в конец UICollectionView. Чтобы показать новые разделы, нужно вызвать reloadData. При вызове этого метода все вычисления для всех текущих доступных разделов выполняются снова! Эта проблема с производительностью вызывает большие заикания при прокрутке списка коллекций, и при прокрутке вниз она становится медленнее и медленнее. Не знаю, можно ли перенести эту работу на фоновый поток. При таком подходе можно прокручивать вверх и вниз, если вы сделаете необходимые адаптации.

Bounty:

Теперь я предлагаю щедрость за ответ на этот вопрос. Меня интересует, как реализуется представление месяца в календаре 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;
        }

        );
    }
}

Если мы приближаемся к вершине, приложение падает с помощью

Снимок снимка, который не был отображен, приводит к пустой снимок. Убедитесь, что ваше представление было просмотрено не ранее, чем раньше моментальный снимок или моментальный снимок после обновлений экрана. Утверждение об отказе в - [Procet_UICollectionViewCell _addUpdateAnimation],/SourceCache/UIKit_Sim/UIKit-2935.137/UICollectionViewCell.m:147

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

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

Ответ 1

После большого количества 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) и все остальное, видимое на экране (кроме навигационной панели и 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

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

Эй! Почему бы вам не сразу разобраться?

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

: - | Я собираюсь, просто повесить там, хорошо!

Итак, как я объяснял, следующая реализация RSDFDatePickerCollectionViewDelegate в 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];
    }
}

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

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-месячный диапазон, он манипулирует источником данных коллекции, будущие 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. Это единственный способ, который дает вам гладкую прокрутку, как Официальное приложение Calendar iOS, я видел, как многие люди манипулировали scrollView и его методом делегатов для достижения бесконечной прокрутки, не видели никакой гладкости там. Дело в том, что манипулирование делегатом UICollectionView приведет к меньшему ущербу, если все сделано правильно, потому что они сделаны для тяжелой работы.

enter image description here

Ответ 2

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

Используйте 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
    )
}

Ответ 3

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