mischa-hildebrand / AlignedCollectionViewFlowLayout

A collection view layout that gives you control over the horizontal and vertical alignment of the cells.
MIT License
1.28k stars 202 forks source link

Multiple Section Header Overlapping Cells #13

Open abkama0a opened 6 years ago

abkama0a commented 6 years ago

I am having a hard time trying to add header sections along with your awesome AlignedcollectionViewFlowLayout, but my attempts are failing.

What I did so far was adding layoutAttributesForSupplementaryView:ofKind:at to your .swift file and modifying your setFrame function to process UICollectionElementKindSectionHeader. I also added a public var for header's height: headerHeight

override open func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {

        guard let attributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
            print("nill called attributes")
            return nil
        }
        print("first cell frame = \(layoutAttributesForItem(at: indexPath)!.frame)")
        let yPos:CGFloat = layoutAttributesForItem(at: indexPath)!.frame.origin.y - headerHeight
        attributes.frame = CGRect(x: 0.0, y: yPos, width: (collectionView?.frame.width)!, height: headerHeight)

        return attributes
    }

and

/// Sets the frame for the passed layout attributes object by calling the `layoutAttributesForItem(at:)` function.
    private func setFrame(forLayoutAttributes layoutAttributes: UICollectionViewLayoutAttributes) {
        if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
            let indexPath = layoutAttributes.indexPath
            if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
                layoutAttributes.frame = newFrame
            }
        } else if layoutAttributes.representedElementCategory == .supplementaryView {
            if layoutAttributes.representedElementKind == UICollectionElementKindSectionHeader {
                if let newFrame = layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: layoutAttributes.indexPath)?.frame {
                    layoutAttributes.frame = newFrame
                }
            }
        }
    }

My implementation seems to work right, but when I segue to my ViewController where I am implementing your custom flow layout, viewForSupplementaryElementOfKind is called too soon before the correct attributes are calculated. I know this because I am triggering a call to viewForSupplementaryElementOfKind by reloadItems(at:) inside collectionView:didSelectItemAt

(Edit: I added the following block to my viewDidAppear to work around this delay

UIView.performWithoutAnimation {
            filterCollection.reloadItems(at: [IndexPath(row: patterns.count - 1, section: 2)])
        }

)

I've also printed frame values for first cell in section as well as the header frame. It seems that my attributes are calculated 4 times and the viewForSupplementaryElementOfKind function takes values from the 3rd call

Edit: see attachment for screens after segueing to VC, log from console about first cell in section and header saved frame, and screen of collectionView after triggering didSelectItemAt or reloadingItems in viewDidAppear

I'd really appreciate it if you'd help me to avoid making extra call to reloadItems(at:)

log after segue to vc after segue to vc after click at item at index

adriantabirta commented 6 years ago

I have the same problem, how to solve it?

abkama0a commented 6 years ago

Here's my current workaround which I think is not a perfect solution:

add the following extension and custom class to your project:

class CVHeader {
    var value: CGFloat = 30.0
}

private var headerKey: UInt8 = 0
extension AlignedCollectionViewFlowLayout {
    var headerHeight: CVHeader {
        get {
            return associatedObject(base: self, key: &headerKey)
            { return CVHeader() }
        }
        set { associateObject(base: self, key: &headerKey, value: newValue) }
    }

    func associatedObject<ValueType: AnyObject>(
        base: AnyObject,
        key: UnsafePointer<UInt8>,
        initialiser: () -> ValueType)
        -> ValueType {
            if let associated = objc_getAssociatedObject(base, key)
                as? ValueType { return associated }
            let associated = initialiser()
            objc_setAssociatedObject(base, key, associated,
                                     .OBJC_ASSOCIATION_RETAIN)
            return associated
    }
    func associateObject<ValueType: AnyObject>(
        base: AnyObject,
        key: UnsafePointer<UInt8>,
        value: ValueType) {
        objc_setAssociatedObject(base, key, value,
                                 .OBJC_ASSOCIATION_RETAIN)
    }

    override open func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let attributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
            return nil
        }

        let yPos:CGFloat = layoutAttributesForItem(at: indexPath)!.frame.origin.y - headerHeight.value
        attributes.frame = CGRect(x: 0.0, y: yPos, width: (collectionView?.frame.width)!, height: headerHeight.value)

        return attributes
    }
}

Then, in your viewController where you are implementing the AlignedCollectionViewFlowLayout, override the viewDidAppear function as follows:

override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        UIView.performWithoutAnimation {
            yourCollectionView.reloadItems(at: [IndexPath(row: yourArray.count - 1, section: yourLastSectionIndex)])
        }
    }

You'll also need to edit the AlignedCollectionViewFlowLayout.swift file. You need to edit the private setFrame function as follows:

private func setFrame(forLayoutAttributes layoutAttributes: UICollectionViewLayoutAttributes) {
        if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
            let indexPath = layoutAttributes.indexPath
            if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
                layoutAttributes.frame = newFrame
            }
        } else if layoutAttributes.representedElementCategory == .supplementaryView {
            if layoutAttributes.representedElementKind == UICollectionElementKindSectionHeader {
                if let newFrame = layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: layoutAttributes.indexPath)?.frame {
                    layoutAttributes.frame = newFrame
                }
            }
        }
    }

if setFrame wasn't private, we could've overridden it in the extension and never had to worry about updating this in the future if the original author update this library. I hope this help you.