web-dev-qa-db-fra.com

En-tête collant UICollectionView dans Swift

J'essaie de créer un en-tête supplémentaire collant, qui reste en haut tout le temps et ne répond pas aux événements de défilement. Les solutions que j’ai trouvées jusqu’à présent réagissent toujours sur le défilement instantané et sont corrigées à l’aide d’un flux personnalisé, ce qui sera probablement également la solution au problème des mines.

La raison pour laquelle je le veux ainsi, c'est que l'en-tête est utilisé à d'autres endroits et doit être réutilisable. J'espère que cela pourra être résolu de cette façon et que je n'aurai pas à créer une vue séparée.

Comme je le fais à Swift, il serait bon d’avoir un exemple à Swift.

14
Antoine

La solution finale que j'ai trouvée:

En utilisant cette présentation de flux personnalisée, il était possible de corriger cet en-tête collant:

class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {

        var superAttributes: [UICollectionViewLayoutAttributes]? = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]

        if superAttributes == nil {
            // If superAttributes couldn't cast, return
            return super.layoutAttributesForElementsInRect(rect)
        }

        let contentOffset = collectionView!.contentOffset
        var missingSections = NSMutableIndexSet()

        for layoutAttributes in superAttributes! {
            if (layoutAttributes.representedElementCategory == .Cell) {
                if let indexPath = layoutAttributes.indexPath {
                    missingSections.addIndex(layoutAttributes.indexPath.section)
                }
            }
        }

        for layoutAttributes in superAttributes! {
            if let representedElementKind = layoutAttributes.representedElementKind {
                if representedElementKind == UICollectionElementKindSectionHeader {
                    if let indexPath = layoutAttributes.indexPath {
                        missingSections.removeIndex(indexPath.section)
                    }
                }
            }
        }

        missingSections.enumerateIndexesUsingBlock { idx, stop in
            let indexPath = NSIndexPath(forItem: 0, inSection: idx)
            if let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath) {
                superAttributes!.append(layoutAttributes)
            }
        }

        for layoutAttributes in superAttributes! {
            if let representedElementKind = layoutAttributes.representedElementKind {
                if representedElementKind == UICollectionElementKindSectionHeader {
                    let section = layoutAttributes.indexPath!.section
                    let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)

                    let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)!
                    let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)!


                    let (firstCellAttributes: UICollectionViewLayoutAttributes, lastCellAttributes: UICollectionViewLayoutAttributes) = {
                        if (self.collectionView!.numberOfItemsInSection(section) > 0) {
                            return (
                                self.layoutAttributesForItemAtIndexPath(firstCellIndexPath),
                                self.layoutAttributesForItemAtIndexPath(lastCellIndexPath))
                        } else {
                            return (
                                self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath),
                                self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath))
                        }
                        }()

                    let headerHeight = CGRectGetHeight(layoutAttributes.frame)
                    var Origin = layoutAttributes.frame.Origin

                    Origin.y = min(contentOffset.y, (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
                    // Uncomment this line for normal behaviour:
                    // Origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))

                    layoutAttributes.zIndex = 1024
                    layoutAttributes.frame = CGRect(Origin: Origin, size: layoutAttributes.frame.size)
                }
            }
        }

        return superAttributes
    }

    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }

}

Pour créer une mise en page où les en-têtes sont collants comme d'habitude, changez cette ligne:

Origin.y = min(contentOffset.y, (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))

à cette ligne:

Origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))

En espérant que cela soit utile pour les autres!

Mettre à jour

Mise à jour pour corriger un crash (merci à Robert Atkins!) Et quelques mises à jour de Swift 1.2

tvOS & iOS 9

tvOS et iOS 9 ont introduit la propriété sectionHeadersPinToVisibleBounds qui peut être utilisée

6
Antoine

La solution la plus simple pour iOS 9 +, car elle n'a pas besoin d'écrire sous-classe UICollectionViewFlowLayout.

Dans viewDidLoad de viewController avec collectionView, utilisez le code suivant:

let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout // casting is required because UICollectionViewLayout doesn't offer header pin. Its feature of UICollectionViewFlowLayout
layout?.sectionHeadersPinToVisibleBounds = true

@ Antoine fait également allusion à cela.

54
Dari

fonctionne pour moi avec Swift 2.0

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    var superAttributes:NSMutableArray = NSMutableArray(array: super.layoutAttributesForElementsInRect(rect)!) as NSMutableArray

    let contentOffset = collectionView!.contentOffset
    var missingSections = NSMutableIndexSet()

    for layoutAttributes in superAttributes {
        if (layoutAttributes.representedElementCategory == .Cell) {
            if let indexPath = layoutAttributes.indexPath {
                missingSections.addIndex(layoutAttributes.indexPath.section)
            }
        }
    }

    for layoutAttributes in superAttributes{
        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                if let indexPath = layoutAttributes.indexPath {
                    missingSections.removeIndex(indexPath.section)
                }
            }
        }
    }

    missingSections.enumerateIndexesUsingBlock { idx, stop in
        let indexPath = NSIndexPath(forItem: 0, inSection: idx)
        let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
        superAttributes.addObject(layoutAttributes!)
    }

    for la in superAttributes {

        let layoutAttributes = la as! UICollectionViewLayoutAttributes;

        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                let section = layoutAttributes.indexPath.section
                let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)

                let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)
                let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)                    

                var firstCellAttributes:UICollectionViewLayoutAttributes
                var lastCellAttributes:UICollectionViewLayoutAttributes

                    if (self.collectionView!.numberOfItemsInSection(section) > 0) {
                            firstCellAttributes = self.layoutAttributesForItemAtIndexPath(firstCellIndexPath)!
                            lastCellAttributes = self.layoutAttributesForItemAtIndexPath(lastCellIndexPath)!
                    } else {
                            firstCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath)!
                            lastCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath)!
                    }

                let headerHeight = CGRectGetHeight(layoutAttributes.frame)
                var Origin = layoutAttributes.frame.Origin

                 Origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
                ;

                layoutAttributes.zIndex = 1024;
                layoutAttributes.frame = CGRect(Origin: Origin, size: layoutAttributes.frame.size)

            }
        }
    }

    return NSArray(array: superAttributes) as? [UICollectionViewLayoutAttributes]
}

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    return true
}
3
Irina Didkovskaya

Swift 2.2 version testée sur la base de la réponse de GregP. Le code de Greg renvoyait une erreur facultative de décompression dans lastCellIndexPath car le nombre de sections est initialement égal à zéro. J'ai donc déplacé le numberOfItemsInSection> 0 check up.

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    let superAttributes:NSMutableArray = NSMutableArray(array: super.layoutAttributesForElementsInRect(rect)!) as NSMutableArray

    let contentOffset = collectionView!.contentOffset
    let missingSections = NSMutableIndexSet()

    for layoutAttributes in superAttributes {
        if (layoutAttributes.representedElementCategory == .Cell) {
            if let _ = layoutAttributes.indexPath {
                missingSections.addIndex(layoutAttributes.indexPath.section)
            }
        }
    }

    for layoutAttributes in superAttributes{
        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                if let indexPath = layoutAttributes.indexPath {
                    missingSections.removeIndex(indexPath.section)
                }
            }
        }
    }

    missingSections.enumerateIndexesUsingBlock { idx, stop in
        let indexPath = NSIndexPath(forItem: 0, inSection: idx)
        let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
        superAttributes.addObject(layoutAttributes!)
    }

    for la in superAttributes {

        let layoutAttributes = la as! UICollectionViewLayoutAttributes;

        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                let section = layoutAttributes.indexPath.section
                let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)

                if numberOfItemsInSection > 0{
                    let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)
                    let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)

                    var firstCellAttributes:UICollectionViewLayoutAttributes
                    var lastCellAttributes:UICollectionViewLayoutAttributes

                    firstCellAttributes = self.layoutAttributesForItemAtIndexPath(firstCellIndexPath)!
                    lastCellAttributes = self.layoutAttributesForItemAtIndexPath(lastCellIndexPath)!

                    let headerHeight = CGRectGetHeight(layoutAttributes.frame)
                    var Origin = layoutAttributes.frame.Origin

                    Origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight));

                    layoutAttributes.zIndex = 1024;
                    layoutAttributes.frame = CGRect(Origin: Origin, size: layoutAttributes.frame.size)
                }
            }
        }
    }

    return NSArray(array: superAttributes) as? [UICollectionViewLayoutAttributes]
}

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    return true
}
2
Ross

J'ai corrigé mon plantage en testant les sections vides comme indiqué dans this Gist . J'ai aussi ajouté quelques if lets pour des points de style Swift supplémentaires ;-). Cela fonctionne maintenant pour moi:

class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {

        var answer: [UICollectionViewLayoutAttributes] = super.layoutAttributesForElementsInRect(rect)! as [UICollectionViewLayoutAttributes]
        let contentOffset = collectionView!.contentOffset

        var missingSections = NSMutableIndexSet()

        for layoutAttributes in answer {
            if (layoutAttributes.representedElementCategory == .Cell) {
                if let indexPath = layoutAttributes.indexPath {
                    missingSections.addIndex(layoutAttributes.indexPath.section)
                }
            }
        }

        for layoutAttributes in answer {
            if let representedElementKind = layoutAttributes.representedElementKind {
                if representedElementKind == UICollectionElementKindSectionHeader {
                    if let indexPath = layoutAttributes.indexPath {
                        missingSections.removeIndex(indexPath.section)
                    }
                }
            }
        }

        missingSections.enumerateIndexesUsingBlock { idx, stop in
            let indexPath = NSIndexPath(forItem: 0, inSection: idx)
            if let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath) {
                answer.append(layoutAttributes)
            }
        }

        for layoutAttributes in answer {
            if let representedElementKind = layoutAttributes.representedElementKind {
                if representedElementKind == UICollectionElementKindSectionHeader {
                    let section = layoutAttributes.indexPath!.section
                    let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)

                    let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)!
                    let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)!


                    let (firstCellAttributes: UICollectionViewLayoutAttributes, lastCellAttributes: UICollectionViewLayoutAttributes) = {
                        if (self.collectionView!.numberOfItemsInSection(section) > 0) {
                            return (
                                self.layoutAttributesForItemAtIndexPath(firstCellIndexPath),
                                self.layoutAttributesForItemAtIndexPath(lastCellIndexPath))
                        } else {
                            return (
                                self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath),
                                self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath))
                        }
                    }()

                    let headerHeight = CGRectGetHeight(layoutAttributes.frame)
                    var Origin = layoutAttributes.frame.Origin

                    Origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))

                    layoutAttributes.zIndex = 1024
                    layoutAttributes.frame = CGRect(Origin: Origin, size: layoutAttributes.frame.size)
                }
            }
        }

        return answer
    }

    override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
        return true
    }

}
2
Robert Atkins

Cleaner Swift 2.3 version testée de la réponse d'Irina

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    guard var superAttributes = super.layoutAttributesForElementsInRect(rect) else {
        return super.layoutAttributesForElementsInRect(rect)
    }

    let contentOffset = collectionView!.contentOffset
    let missingSections = NSMutableIndexSet()

    for layoutAttributes in superAttributes {
        if (layoutAttributes.representedElementCategory == .Cell) {
            missingSections.addIndex(layoutAttributes.indexPath.section)
        }

        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                missingSections.removeIndex(layoutAttributes.indexPath.section)
            }
        }
    }

    missingSections.enumerateIndexesUsingBlock { idx, stop in
        let indexPath = NSIndexPath(forItem: 0, inSection: idx)
        if let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath) {
            superAttributes.append(layoutAttributes)
        }
    }

    for layoutAttributes in superAttributes {
        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                let section = layoutAttributes.indexPath.section
                let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)

                let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)
                let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)

                var firstCellAttributes:UICollectionViewLayoutAttributes
                var lastCellAttributes:UICollectionViewLayoutAttributes

                if (self.collectionView!.numberOfItemsInSection(section) > 0) {
                    firstCellAttributes = self.layoutAttributesForItemAtIndexPath(firstCellIndexPath)!
                    lastCellAttributes = self.layoutAttributesForItemAtIndexPath(lastCellIndexPath)!
                } else {
                    firstCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath)!
                    lastCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath)!
                }

                let headerHeight = CGRectGetHeight(layoutAttributes.frame)
                var Origin = layoutAttributes.frame.Origin

                Origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
                ;

                layoutAttributes.zIndex = 1024;
                layoutAttributes.frame = CGRect(Origin: Origin, size: layoutAttributes.frame.size)

            }
        }
    }

    return superAttributes
}
1
GregP

Voici une solution simple qui fonctionne si vous n’avez qu’une section.

class StickyHeaderLayout: UICollectionViewFlowLayout {

  override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    return true
  }

  override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
    var attributes = super.layoutAttributesForElementsInRect(rect)! as! [UICollectionViewLayoutAttributes]

      let offset          = collectionView?.contentOffset


      for attrs in attributes {
        if attrs.representedElementKind == nil {
          let indexPath        = NSIndexPath(forItem: 0, inSection: attrs.indexPath.section)
          let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)

          attributes.append(layoutAttributes)
        }
      }

      for attrs in attributes {
        if attrs.representedElementKind == nil {
          continue
        }

        if attrs.representedElementKind == UICollectionElementKindSectionHeader {

          var headerRect = attrs.frame
          headerRect.size.height = headerHeight
          headerRect.Origin.y = offset!.y
          attrs.frame = headerRect
          attrs.zIndex = 1024
          break
        }
      }

    return attributes
  }
}
1
Nick Wargnier

La version 3 de Swift essayant d'éviter! Est ce qui a du sens.

class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard var superAttributes = super.layoutAttributesForElements(in: rect), let collectionView = collectionView else {
            return super.layoutAttributesForElements(in: rect)
        }

        let collectionViewTopY = collectionView.contentOffset.y + collectionView.contentInset.top
        let contentOffset = CGPoint(x: 0, y: collectionViewTopY)
        let missingSections = NSMutableIndexSet()

        superAttributes.forEach { layoutAttributes in
            if layoutAttributes.representedElementCategory == .cell && layoutAttributes.representedElementKind != UICollectionElementKindSectionHeader {
                missingSections.add(layoutAttributes.indexPath.section)
            }
        }

        missingSections.enumerate(using: { idx, stop in
            let indexPath = IndexPath(item: 0, section: idx)
            if let layoutAttributes = self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: indexPath) {
                superAttributes.append(layoutAttributes)
            }
        })

        for layoutAttributes in superAttributes {
            if let representedElementKind = layoutAttributes.representedElementKind {
                if representedElementKind == UICollectionElementKindSectionHeader {
                    let section = layoutAttributes.indexPath.section
                    let numberOfItemsInSection = collectionView.numberOfItems(inSection: section)

                    let firstCellIndexPath = IndexPath(item: 0, section: section)
                    let lastCellIndexPath = IndexPath(item: max(0, (numberOfItemsInSection - 1)), section: section)                   

                    let cellAttributes:(first: UICollectionViewLayoutAttributes, last: UICollectionViewLayoutAttributes) = {
                        if (collectionView.numberOfItems(inSection: section) > 0) {
                            return (
                                self.layoutAttributesForItem(at: firstCellIndexPath)!,
                                self.layoutAttributesForItem(at: lastCellIndexPath)!)
                        } else {
                            return (
                                self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: firstCellIndexPath)!,
                                self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionFooter, at: lastCellIndexPath)!)
                        }
                    }()

                    let headerHeight = layoutAttributes.frame.height
                    var Origin = layoutAttributes.frame.Origin
                    // This line makes only one header visible fixed at the top
//                    Origin.y = min(contentOffset.y, cellAttributes.last.frame.maxY - headerHeight)
                    // Uncomment this line for normal behaviour:
                    Origin.y = min(max(contentOffset.y, cellAttributes.first.frame.minY - headerHeight), cellAttributes.last.frame.maxY - headerHeight)

                    layoutAttributes.zIndex = 1024
                    layoutAttributes.frame = CGRect(Origin: Origin, size: layoutAttributes.frame.size)
                }
            }
        }

        return superAttributes
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
}
0
user12345625