it-roy-ru.com

Липкий заголовок UICollectionView в swift

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

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

Поскольку я делаю это в Swift, было бы здорово иметь пример в Swift.

14
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))

Надеюсь, что это полезно для других!

Обновление

Обновлено, чтобы исправить сбой (спасибо Роберту Аткинсу!) И некоторые обновления Swift 1.2

tvOS и iOS 9

tvOS и iOS 9 представили свойство sectionHeadersPinToVisibleBounds, которое можно использовать

6
Antoine

Самое простое решение для iOS 9 +, так как не требует написания подкласса UICollectionViewFlowLayout.

В viewDidLoad viewController с collectionView используйте следующий код:

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

На это намекает и @Antoine.

54
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
Irina Didkovskaya

Версия Swift 2.2 протестирована на основе ответа GregP. Код Грега выбрасывал необязательную ошибку в lastCellIndexPath , потому что номер раздела изначально равен нулю. Поэтому я переместил numberOfItemsInSection> 0 проверьте.

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

Я исправил мой сбой, протестировав тестирование на пустые разделы, как предложено в этом 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

Очиститель 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

Вот простое решение, которое работает, если у вас есть только один раздел.

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