web-dev-qa-db-ja.com

swiftのUICollectionViewスティッキーヘッダー

スティッキー補足ヘッダーを作成しようとしています。これは常に一番上にあり、スクロールイベントに応答しません。私がこれまでに見つけた解決策はまだブートスクロールに反応し、カスタムのflowLayoutを使用して修正されます。これはおそらく私の問題の修正にもなります。

このようにしたい理由は、ヘッダーが他の場所で使用されており、再利用可能であるべきだからです。これがこの方法で解決され、別のビューを作成する必要がないことを願っています。

私がSwiftでこれを行っているので、Swiftで例を示すことは素晴らしいことです。

16
Antoine

私が見つけた最終的な解決策:

このカスタムフローレイアウトを使用して、このスティッキーヘッダーを修正することができました。

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
    }

}

ヘッダーが従来のようにスティッキーなレイアウトを作成するには、次の行を変更します。

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

この行に:

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

これが他の人に役立つことを願っています!

更新

クラッシュを修正するために更新(Robert Atkinsに感謝!)およびSwift 1.2へのいくつかの更新

tvOS&iOS 9

tvOSおよびiOS 9で使用できるsectionHeadersPinToVisibleBoundsプロパティが導入されました

7
Antoine

UICollectionViewFlowLayoutのサブクラスを記述する必要がないため、iOS 9 +の最も簡単なソリューション。

CollectionViewを含むviewControllerのviewDidLoadでは、次のコードを使用します。

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

@Antoineからもヒントを得ています。

69
Dari

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

GregPの回答に基づいてテストされたSwift 2.2バージョン。セクション数が最初はゼロであるため、グレッグのコードはlastCellIndexPathでアンラップオプションエラーをスローしていました。それで、numberOfItemsInSection>チェックアップを移動しました。

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

this Gist で提案されているように、空のセクションのテストをテストしてクラッシュを修正しました。いくつかのif letsを追加して、Swiftスタイルポイント;-)を追加しました。これは今私のために働きます:

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テスト済みバージョンのイリーナの答え

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

セクションが1つしかない場合に機能する簡単なソリューションを次に示します。

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

!を回避しようとするSwift 3バージョンは、理にかなっています。

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