Como tornar a exibição suplementar flutuar no UICollectionView como os headers de seção fazem no estilo simples do UITableView

Estou lutando para obter um efeito de “header de seção flutuante” com o UICollectionView . Algo que tem sido bastante fácil no UITableView (comportamento padrão para UITableViewStylePlain ) parece impossível no UICollectionView sem muito trabalho duro. Eu estou sentindo falta do óbvio?

A Apple não fornece documentação sobre como conseguir isso. Parece que é preciso subclassificar o UICollectionViewLayout e implementar um layout personalizado apenas para obter esse efeito. Isso requer muito trabalho, implementando os seguintes methods:

Métodos para replace

Cada object de layout deve implementar os seguintes methods:

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

No entanto, não está claro para mim como fazer a visão suplementar flutuar acima das células e “grudar” no topo da visão até que a próxima seção seja alcançada. Existe um sinalizador para isso nos atributos de layout?

Eu teria usado o UITableView mas preciso criar uma hierarquia bastante complexa de collections que seja facilmente obtida com uma visão de coleção.

Qualquer orientação ou código de exemplo seria muito apreciado!

    No iOS9, a Apple teve a gentileza de adicionar uma propriedade simples em UICollectionViewFlowLayout chamada sectionHeadersPinToVisibleBounds .

    Com isso, você pode fazer com que os headers flutuem assim em visualizações de tabela.

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

    Você pode implementar os seguintes methods de delegação:

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

    Em seu controlador de modo de exibição que possui seu método :cellForItemAtIndexPath (apenas retorne os valores corretos). Ou, em vez de usar os methods delegates, você também pode definir esses valores diretamente no object de layout, por exemplo, [layout setItemSize:size]; .

    Usando qualquer um desses methods, você poderá definir suas configurações em Código, em vez de IB, à medida que forem removidas quando você definir um Layout personalizado. Lembre-se de adicionar ao seu arquivo .h também!

    Crie uma nova subclass de UICollectionViewFlowLayout , chame como quiser e verifique se o arquivo H possui:

     #import  @interface YourSubclassNameHere : UICollectionViewFlowLayout @end 

    Dentro do Arquivo de Implementação, verifique se ele tem o seguinte:

     - (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; } 

    Escolha “Custom” no Interface Builder para o Flow Layout, escolha a sua class “YourSubclassNameHere” que você acabou de criar. E corra!

    (Nota: o código acima pode não respeitar os valores de contentInset.bottom, ou especialmente os objects de rodapé grandes ou pequenos, ou collections que tenham 0 objects, mas nenhum rodapé.)

    Aqui está a minha opinião, eu acho que é muito mais simples do que um vislumbre acima. A principal fonte de simplicidade é que eu não estou subclassificando o layout do stream, e sim desenvolvendo meu próprio layout (muito mais fácil, se você me perguntar).

    Por favor, note que estou supondo que você já é capaz de implementar seu próprio UICollectionViewLayout personalizado que exibirá células e headers sem flutuante implementado. Depois de ter essa implementação escrita, só então o código abaixo fará algum sentido. Novamente, isso ocorre porque o OP estava perguntando especificamente sobre a parte de headers flutuantes.

    alguns bônus:

    1. Eu estou flutuando dois headers, não apenas um
    2. Cabeçalhos empurram headers anteriores para fora do caminho
    3. Olhe rápido!

    Nota:

    1. supplementaryLayoutAttributes contém todos os atributos de header sem implementação flutuante
    2. Eu estou usando este código em prepareLayout , desde que eu faço toda a computação adiantada.
    3. não se esqueça de replace shouldInvalidateLayoutForBoundsChange para 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 } 

    Se você tiver uma única visualização de header que deseja fixar na parte superior de seu UICollectionView, veja uma maneira relativamente simples de fazer isso. Observe que isso deve ser o mais simples possível – ele pressupõe que você esteja usando um único header em uma única seção.

     //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's 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 

    Veja: https://gist.github.com/4613982

    Se já tiver definido o layout de stream no Storyboard ou Xib arquivo Xib , tente isso,

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

    No caso de alguém estar procurando uma solução no Objective-C, coloque isso em viewDidload:

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

    Eu encontrei o mesmo problema e achei isso nos meus resultados do google. Primeiramente, gostaria de agradecer à cocotutch por compartilhar sua solução. No entanto, eu queria que meu UICollectionView rolasse horizontalmente e os headers fiquem à esquerda da canvas, então tive que mudar um pouco a solução.

    Basicamente eu apenas mudei isso:

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

    para isso:

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

    Veja: https://gist.github.com/vigorouscoding/5155703 ou http://www.vigorouscoding.com/2013/03/uicollectionview-with-sticky-headers/

    Eu corri isso com código de codificação vigorosa . No entanto, esse código não considerou sectionInset.

    Então eu mudei este código para rolagem vertical

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

    para

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

    Se vocês querem código para rolagem horizontal, consulte o código aove.

    Há um erro no post do cocotouch. Quando não houver itens na seção e o rodapé da seção estiver definido para não ser exibido, o header da seção ficará fora da visualização da coleção e o usuário não poderá vê-lo.

    Na verdade, mude:

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

    para dentro:

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

    vai resolver esse problema.

    Eu adicionei uma amostra no github que é bem simples, eu acho.

    Basicamente, a estratégia é fornecer um layout personalizado que invalida a mudança de limites e forneça atributos de layout para a exibição suplementar que abraça os limites atuais. Como outros sugeriram. Espero que o código seja útil.

    VCollectionViewGridLayout faz headers fixos . É um layout de grade simples de rolagem vertical com base em TLIndexPathTools . Tente executar o projeto de amostra Sticky Headers .

    Esse layout também possui um comportamento de animação de atualização em lote muito melhor que o UICollectionViewFlowLayout . Há alguns exemplos de projetos fornecidos que permitem alternar entre os dois layouts para demonstrar a melhoria.