Как сделать дополнительный просмотр float в UICollectionView в виде заголовков секций в обычном стиле UITableView

Я пытаюсь добиться эффекта "плавающего раздела" с помощью UICollectionView. То, что было достаточно легко в UITableView (поведение по умолчанию для UITableViewStylePlain), кажется невозможным в UICollectionView без большой тяжелой работы. Я пропустил очевидное?

Apple не предоставляет документации о том, как этого добиться. Кажется, что нужно подклассом UICollectionViewLayout и реализовать пользовательский макет только для достижения этого эффекта. Это влечет за собой довольно много работы, реализуя следующие методы:

Методы переопределения

Каждый объект макета должен реализовывать следующие методы:

collectionViewContentSize
layoutAttributesForElementsInRect:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath: (if your layout supports supplementary views)
layoutAttributesForDecorationViewOfKind:atIndexPath: (if your layout supports decoration views)
shouldInvalidateLayoutForBoundsChange:

Однако его неясно, как сделать дополнительный вид плавающим над ячейками и "придерживаться" в верхней части представления до тех пор, пока не будет достигнут следующий раздел. Есть ли флаг для этого в атрибутах макета?

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

Приветствуется любое руководство или образец кода!

Ответ 1

Я искал тот же эффект, но в iOS9 Apple был достаточно любезен, чтобы добавить простое свойство в UICollectionViewFlowLayout, называемое sectionHeadersPinToVisibleBounds

Справочник Apple Doc

С этим вы можете сделать заголовки плавающими, как Tableviews.

    let layout = UICollectionViewFlowLayout()
    layout.sectionHeadersPinToVisibleBounds = true
    layout.minimumInteritemSpacing = 1
    layout.minimumLineSpacing = 1
    super.init(collectionViewLayout: layout)

Ответ 2

Либо реализовать следующие методы делегата:

– collectionView:layout:sizeForItemAtIndexPath:
– collectionView:layout:insetForSectionAtIndex:
– collectionView:layout:minimumLineSpacingForSectionAtIndex:
– collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
– collectionView:layout:referenceSizeForHeaderInSection:
– collectionView:layout:referenceSizeForFooterInSection:

В вашем представлении контроллер, у которого есть ваш метод :cellForItemAtIndexPath (просто верните правильные значения). Или, вместо использования методов делегатов, вы также можете установить эти значения непосредственно в свой объект макета, например. [layout setItemSize:size];.

Использование любого из этих методов позволит вам установить свои настройки в коде, а не в IB, поскольку они удаляются при настройке настраиваемого макета. Не забудьте добавить <UICollectionViewDelegateFlowLayout> в ваш .h файл тоже!

Создайте новый подкласс UICollectionViewFlowLayout, вызовите его, как хотите, и убедитесь, что в файле H:

#import <UIKit/UIKit.h>

@interface YourSubclassNameHere : UICollectionViewFlowLayout

@end

Внутри файла реализации убедитесь, что он имеет следующее:

- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {

    NSMutableArray *answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
    UICollectionView * const cv = self.collectionView;
    CGPoint const contentOffset = cv.contentOffset;

    NSMutableIndexSet *missingSections = [NSMutableIndexSet indexSet];
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
            [missingSections addIndex:layoutAttributes.indexPath.section];
        }
    }
    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
            [missingSections removeIndex:layoutAttributes.indexPath.section];
        }
    }

    [missingSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {

        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];

        UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];

        [answer addObject:layoutAttributes];

    }];

    for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {

        if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {

            NSInteger section = layoutAttributes.indexPath.section;
            NSInteger numberOfItemsInSection = [cv numberOfItemsInSection:section];

            NSIndexPath *firstCellIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            NSIndexPath *lastCellIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];

            NSIndexPath *firstObjectIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
            NSIndexPath *lastObjectIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];

            UICollectionViewLayoutAttributes *firstObjectAttrs;
            UICollectionViewLayoutAttributes *lastObjectAttrs;

            if (numberOfItemsInSection > 0) {
                firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
                lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
            } else {
                firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                                    atIndexPath:firstObjectIndexPath];
                lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                                   atIndexPath:lastObjectIndexPath];
            }

            CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.y = MIN(
                           MAX(
                               contentOffset.y + cv.contentInset.top,
                               (CGRectGetMinY(firstObjectAttrs.frame) - headerHeight)
                               ),
                           (CGRectGetMaxY(lastObjectAttrs.frame) - headerHeight)
                           );

            layoutAttributes.zIndex = 1024;
            layoutAttributes.frame = (CGRect){
                .origin = origin,
                .size = layoutAttributes.frame.size
            };

        }

    }

    return answer;

}

- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound {

    return YES;

}

Выберите "Пользовательский" в Interface Builder для макета потока, выберите свой класс "YourSubclassNameHere", который вы только что создали. И Run!

(Примечание: приведенный выше код может не учитывать значения contentInset.bottom или особенно большие или маленькие нижние объекты или коллекции с 0 объектами, но без нижнего колонтитула.)

Ответ 3

Вот мой взгляд на это, я думаю, это намного проще, чем то, что было замечено выше. Основной источник простоты в том, что я не подклассифицирую раскладку потока, а сворачиваю свой собственный макет (гораздо проще, если вы спросите меня).

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

несколько бонусов:

  • Я плаваю два заголовка, а не только один
  • Заголовки сбрасывают предыдущие заголовки.
  • Посмотрите, быстро!

Примечание:

  • supplementaryLayoutAttributes содержит все атрибуты заголовка без плавающей реализации
  • Я использую этот код в prepareLayout, так как я все вычисляю заранее.
  • не забывайте переопределить shouldInvalidateLayoutForBoundsChange на true!

// float them headers
let yOffset = self.collectionView!.bounds.minY
let headersRect = CGRect(x: 0, y: yOffset, width: width, height: headersHeight)

var floatingAttributes = supplementaryLayoutAttributes.filter {
    $0.frame.minY < headersRect.maxY
}

// This is three, because I am floating 2 headers
// so 2 + 1 extra that will be pushed away
var index = 3
var floatingPoint = yOffset + dateHeaderHeight

while index-- > 0 && !floatingAttributes.isEmpty {

    let attribute = floatingAttributes.removeLast()
    attribute.frame.origin.y = max(floatingPoint, attribute.frame.origin.y)

    floatingPoint = attribute.frame.minY - dateHeaderHeight
}

Ответ 4

Если у вас есть одно представление заголовка, которое вы хотите привязать к вершине вашего UICollectionView, здесь достаточно простой способ сделать это. Обратите внимание, что это должно быть как можно более простым - предполагается, что вы используете один заголовок в одном разделе.

//Override UICollectionViewFlowLayout class
@interface FixedHeaderLayout : UICollectionViewFlowLayout
@end

@implementation FixedHeaderLayout
//Override shouldInvalidateLayoutForBoundsChange to require a layout update when we scroll 
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

//Override layoutAttributesForElementsInRect to provide layout attributes with a fixed origin for the header
- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {

    NSMutableArray *result = [[super layoutAttributesForElementsInRect:rect] mutableCopy];

    //see if there already a header attributes object in the results; if so, remove it
    NSArray *attrKinds = [result valueForKeyPath:@"representedElementKind"];
    NSUInteger headerIndex = [attrKinds indexOfObject:UICollectionElementKindSectionHeader];
    if (headerIndex != NSNotFound) {
        [result removeObjectAtIndex:headerIndex];
    }

    CGPoint const contentOffset = self.collectionView.contentOffset;
    CGSize headerSize = self.headerReferenceSize;

    //create new layout attributes for header
    UICollectionViewLayoutAttributes *newHeaderAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
    CGRect frame = CGRectMake(0, contentOffset.y, headerSize.width, headerSize.height);  //offset y by the amount scrolled
    newHeaderAttributes.frame = frame;
    newHeaderAttributes.zIndex = 1024;

    [result addObject:newHeaderAttributes];

    return result;
}
@end

Смотрите: https://gist.github.com/4613982

Ответ 5

Я столкнулся с той же проблемой и нашел это в моих результатах Google. Сначала я хотел бы поблагодарить cocotutch за то, что он поделился своим решением. Тем не менее, я хотел, чтобы мой UICollectionView прокручивался по горизонтали, а заголовки располагались слева от экрана, поэтому мне пришлось немного изменить решение.

В основном я просто изменил это:

        CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
        CGPoint origin = layoutAttributes.frame.origin;
        origin.y = MIN(
                       MAX(
                           contentOffset.y,
                           (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)
                           ),
                       (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
                       );

        layoutAttributes.zIndex = 1024;
        layoutAttributes.frame = (CGRect){
            .origin = origin,
            .size = layoutAttributes.frame.size
        };

:

        if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
            CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.y = MIN(
                           MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)),
                           (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
                           );

            layoutAttributes.zIndex = 1024;
            layoutAttributes.frame = (CGRect){
                .origin = origin,
                .size = layoutAttributes.frame.size
            };
        } else {
            CGFloat headerWidth = CGRectGetWidth(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.x = MIN(
                           MAX(contentOffset.x, (CGRectGetMinX(firstCellAttrs.frame) - headerWidth)),
                           (CGRectGetMaxX(lastCellAttrs.frame) - headerWidth)
                           );

            layoutAttributes.zIndex = 1024;
            layoutAttributes.frame = (CGRect){
                .origin = origin,
                .size = layoutAttributes.frame.size
            };
        }

Смотрите: https://gist.github.com/vigorouscoding/5155703 или http://www.vigorouscoding.com/2013/03/uicollectionview-with-sticky-headers/

Ответ 6

Я запускал это с помощью кода сильного кодирования. Однако этот код не рассматривал sectionInset.

Итак, я изменил этот код для вертикальной прокрутки

origin.y = MIN(
              MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)),
              (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
           );

to

origin.y = MIN(
           MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight - self.sectionInset.top)),
           (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight + self.sectionInset.bottom)
           );

Если вам нужен код для горизонтального прокрутки, обратитесь к коду.

Ответ 7

В сообщении cocotouch есть ошибка. Когда в разделе нет элементов, а нижний колонтитул секции не отображается, заголовок секции выходит за пределы представления коллекции, и пользователь не сможет его увидеть.

Фактически изменение:

if (numberOfItemsInSection > 0) {
    firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
    lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
} else {
    firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                            atIndexPath:firstObjectIndexPath];
    lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                           atIndexPath:lastObjectIndexPath];
}

в

if (numberOfItemsInSection > 0) {
    firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
    lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
} else {
    firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                            atIndexPath:firstObjectIndexPath];
    lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                           atIndexPath:lastObjectIndexPath];
    if (lastObjectAttrs == nil) {
        lastObjectAttrs = firstObjectAttrs;
    }
}

решит эту проблему.

Ответ 8

Если уже установлен макет потока в файле Storyboard или Xib, попробуйте это,

(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionHeadersPinToVisibleBounds = true

Ответ 9

Я добавил пример github, который, по-моему, довольно прост.

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

Ответ 10

Если кто-то ищет решение в Objective-C, поместите это в viewDidload:

    UICollectionViewFlowLayout *flowLayout = 
    (UICollectionViewFlowLayout*)_yourcollectionView.collectionViewLayout;
    [flowLayout setSectionHeadersPinToVisibleBounds:YES];

Ответ 11

VCollectionViewGridLayout содержит липкие заголовки. Это вертикальная прокрутка простой сетки, основанной на TLIndexPathTools. Попробуйте запустить образец Sticky Headers.

В этом макете также улучшено поведение анимации пакетного обновления, чем UICollectionViewFlowLayout. Есть несколько примеров проектов, которые позволяют вам переключаться между двумя макетами, чтобы продемонстрировать улучшение.