포커스에 따라 위아래로 움직이는 Sticky 섹션 헤더 구현

Apple 근제 앱을 보면, 컬렉션 뷰의 헤더가 이런 움직임을 하고 있었습니다.



이것을 구현하고 싶습니다. 게다가 어차피라면 여러 섹션 있는 경우에는 Sticky인 헤더를 실현하고 싶네요.
라이브러리가 있을까라고 생각하면 의외로 횡방향이 없었기 때문에, UICollectionView의 커스텀 Layout로서 만들었습니다.
iOS에도 대응하고 있습니다.

toshi0383/HorizontalStickyHeaderLayout



먼저 제공되는 다음 6개의 delegate를 구현합니다. 각 셀과 헤더의 크기와 여백을 지정합니다.
@objc
public protocol HorizontalStickyHeaderLayoutDelegate: class {
    func collectionView(_ collectionView: UICollectionView, hshlSizeForItemAtIndexPath indexPath: IndexPath) -> CGSize
    func collectionView(_ collectionView: UICollectionView, hshlSectionInsetsAtSection section: Int) -> UIEdgeInsets
    func collectionView(_ collectionView: UICollectionView, hshlMinSpacingForCellsAtSection section: Int) -> CGFloat
    func collectionView(_ collectionView: UICollectionView, hshlSizeForHeaderAtSection section: Int) -> CGSize
    func collectionView(_ collectionView: UICollectionView, hshlHeaderInsetsAtSection section: Int) -> UIEdgeInsets
    @objc optional func collectionView(_ collectionView: UICollectionView, hshlDidUpdatePoppingHeaderIndexPaths indexPaths: [IndexPath])
}

포커스로 위아래로 움직이는 애니메이션은 스스로 구현합니다.
왜냐하면, 커스텀 레이아웃내에서 레이아웃 처리를 하고 있는 뷰의 좌표를 바꾸면 화나기 때문입니다. 즉, 아래와 같이 헤더 뷰의 subview를 애니메이션 시킬 필요가 있습니다.
애니메이션 시키는 타이밍에 대해서는 hshlDidUpdatePoppingHeaderIndexPaths 의 delegate 로 통지됩니다만, 레이아웃측은 포커스의 갱신을 검지할 필요가 없기 때문에, didUpdateFocus 의 타이밍에서도 재계산시켜 애니메이션 시키면 됩니다.
    // Popping Header
    func collectionView(_ collectionView: UICollectionView, hshlDidUpdatePoppingHeaderIndexPaths indexPaths: [IndexPath]) {
        let (pop, unpop) = self.getHeaders(poppingHeadersIndexPaths: self.layout.poppingHeaderIndexPaths)
        UIView.animate(withDuration: Const.unpopDuration, delay: 0, options: [.curveEaseOut], animations: {
            unpop.forEach { $0.unpopHeader() }
            pop.forEach { $0.popHeader() }
        }, completion: nil)
    }

    override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
        layout.updatePoppingHeaderIndexPaths()
        let (pop, unpop) = self.getHeaders(poppingHeadersIndexPaths: self.layout.poppingHeaderIndexPaths)
        UIView.animate(withDuration: Const.unpopDuration, delay: 0, options: [.curveEaseOut], animations: {
            unpop.forEach { $0.unpopHeader() }
        }, completion: nil)
        coordinator.addCoordinatedAnimations({
            pop.forEach { $0.popHeader() }
        }, completion: nil)
        super.didUpdateFocus(in: context, with: coordinator)
    }

    private func getHeaders(poppingHeadersIndexPaths indexPaths: [IndexPath]) -> (pop: [HeaderView], unpop: [HeaderView]) {
        var visible = collectionView.visibleSupplementaryViews(ofKind: UICollectionElementKindSectionHeader)
        var pop: [HeaderView] = []
        for indexPath in indexPaths {
            guard let view = collectionView.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: indexPath) else {
                continue
            }
            if let index = visible.index(of: view) {
                visible.remove(at: index)
            }
            if let header = view as? HeaderView {
                pop.append(header)
            }
        }
        return (pop: pop, unpop: visible.flatMap { $0 as? HeaderView })
    }
popHeader() unpopHeader() 중에서는 HeaderView의 subview의 좌표를 바꾸고 있습니다.

이제 포커스에 따라 위아래로 움직이는 섹션 헤더를 구현할 수있었습니다. 여러 섹션의 경우의 Sticky적인 거동은 아무것도 하지 않아도 실현되고 있습니다.



요약



포커스에 맞추어 상하로 움직이는 Sticky인 섹션 헤더를 구현해 보았습니다.
htps : // 기주 b. 코 m / 0383

처음에는 레이아웃 측에서 헤더의 좌표를 바꾸어 collectionView.layoutIfNeeded() 를 애니메이션 블록에 넣는다는 접근이었습니다만, 셀마다 표시되지 않게 되는 문제가 있거나 했기 때문에 위와 같은 구현에 침착하고 있습니다.

tvOS 앱을 개발할 기회가 있으면 거의 반드시 이런 레이아웃은 한다고 생각하므로, 꼭 도입해 보세요.

좋은 웹페이지 즐겨찾기