9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

tvOSAdvent Calendar 2017

Day 6

フォーカスに合わせて上下に動くStickyなセクションヘッダーを実装する

Last updated at Posted at 2017-12-05

Apple謹製アプリを見てみると、コレクションビューのヘッダーがこういう動きをしていました。

popping-sticky-header.gif

これ実装したいですよね。しかもどうせなら複数セクションある場合には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-animated-header-for-tvos.gif

まとめ

フォーカスに合わせて上下に動くStickyなセクションヘッダーを実装してみました。
https://github.com/toshi0383/HorizontalStickyHeaderLayout

最初はレイアウト側でヘッダーの座標を変えてcollectionView.layoutIfNeeded()をアニメーションブロックに入れるというアプローチだったのですが、セルごと表示されなくなる不具合があったりしたため上記のような実装に落ち着いています。

tvOSアプリを開発する機会があればほぼ必ずこういうレイアウトはすると思うので、是非取り入れてみてください。

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?