【Swift】CompotionalLayouts でセクションごとのcollectionView/cellにアクセスする方法

暑いですね、しょーです。

前々回と前回に引き続きCollectionViewのCompositionalLayoutsの理解を深めるマンになってます。

【Swift】iOS13のCollectionView、CompositionalLayoutsでモダンにレイアウトする
【Swift】Compositinal Layouts を複数の型に適応させる方法

今回もCompositionalLayoutsを使用する上でのTipsを紹介しようと思います。

sectionで作成した埋め込みCollectionViewにdelegateメソッドが使えない件

前々回の基本的な使い方で作成したCollectionViewはこのようなレイアウトになっています。

layout

このセクション1に値するCollectionViewに対して、scrollViewDidEndDragging(_:)などのデリゲートメソッドを使用したい時がありました。

しかしCompositionalLayoutsを実装する場合、一つのCollectioinViewインスタンスを使用してセクションごとにレイアウトを組んでいます。
上記の実装の場合セクションが2つありますが、厳密にはCollectionViewを内部に埋め込んでいるわけではありません。
なのでセクションごとのCollectionViewにdelegateを行う、という概念では解決できません。(delagateメソッドは使用できます)

ヒエラルキーを見てみましょう。

内部はCollectionViewではなくprivateなscrollView

階層を確認してみると、横スクロール部分はこのようになっています。

_UICollectionViewOrthogonalScrollerEmbeddedScrollViewというアクセスができないプライベートなScrollViewとしてレイアウトされているのがわかります。

この時点でcollectionView.delegateの付与対象でないことがわかりますね。

visibleItemsInvalidationHandler を利用する

解決策の一つとして、NSCollectionLayoutSectionでsectionを作成する際、section.visibleItemsInvalidationHandlerを利用することで解決できます。
やりたいことによって解決できることが限られるかもしれませんが、いくつか例で検証してみよう!

特定のセクションの特定のセルにアクセスしたい場合

例えば、現在表示されているセルにアクセスしてアニメーション等を設定したい場合。

private func generateHorizontalLayout() -> NSCollectionLayoutSection {
        //  item
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(470))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        // group
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(470))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)

        // secction
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .paging

        // sectionに対してvisibleItemsInvalidationHandlerを使用することでvisibleItemsにアクセスできる
        section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
            visibleItems.forEach({ item in
            guard let cell = collectionView.cellForItem(at: item.indexPath) else { return }
          UIView.animate(withDuration: 0.3) {
                  cell.transform = ...
             }
       })
        }

        return section
}

上記のようにすると表示されているセルに対してアニメーションを設定できたりします。

スクロールを検知してpageControlの表示と表示セルのインデックスを同期したい場合

自分がハマっていたのはこちらなのですが、私は冒頭でも書いたscrollViewのデリゲートメソッドを利用してpageControl.currentPageに現在表示されているセルのインデックスを当てはめたかったのです。
その場合もvisibleItemsInvalidationHandlerを使うことで解決できました。

private func generateHorizontalLayout() -> NSCollectionLayoutSection {
        //  item
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(470))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        // group
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(470))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)

        // secction
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .paging

        // sectionに対してvisibleItemsInvalidationHandlerを使用することでvisibleItemsにアクセスできる
   section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
            self?.pageControl.currentPage = visibleItems.last!.indexPath.row
        }

        return section
}

単純にvisibleItemsを利用することで、表示セルのインデックスが取得できます。
こちらをpageControl.currentPageに当てはめることで反映されます。

横スクロールセルのタップ検知

この場合は普通のcollectionView.delegateで解決できます。
単純にdidSelectItemAtを利用し、中でindexPatth.sectionを使用することで各セクションのセルにアクセスできます。

enum Section: CaseIterable {
    case firstSection
    case secondSection
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let sectionType = Section.allCases[indexPath.section]
        switch sectionType {
        case .firstSection:
            let cell = self.collectionView.dequeueReusableCell(for: indexPath, as: ApplianceHorizontalCell.self)
            cell.delegate = self

        case .secondSection:
            let cell = self.collectionView.dequeueReusableCell(for: indexPath, as: ArticleCell.self)
            cell.delegate = self
        }
}

CompositionalLayoutsを実装するときはenumでセクションごとに分けて実装すると思うので、そのenumごとに分岐をすれば横スクロールの内蔵されたセルにも問題なくアクセスが可能です。

まとめ

頭に入れておきたいのはまとめると

  • CompositionalLayoutsを実装する際、横スクロール&縦スクロールなど複数セクションを持っている場合はその分のCollectionViewが作成されているわけでは無い
  • didSelectItemAtなどのCollectionViewベースのdelagateメソッドはenum + indexPath.sectionの分岐で解決できる
  • scrollviewベースのdelegateメソッド(endDraggingなど)はセクションへのアクセスが難しいのでvisibleItemsInvalidationHandlerを利用する

の3つですかね。

この辺を抑えていれば、ある程度特定セルの対応は行えそうです!

いやいや、だいぶCollectionViewに詳しくなってきましたね。
この調子で使い倒していきたいと思います。

参考になれば幸いだぜ。

今日は以上だ、3話連続でCollectionViewの話題だったな!

お疲れっす!
じゃーな!