9
8

More than 3 years have passed since last update.

[iOS]モダンUICollectionViewを最速で理解して複雑な画面を作る(iOS13〜)

Last updated at Posted at 2020-11-07

SwiftUIの進捗どうですか?私は進捗ダメです。

それはともかく、iOS13の登場とともに一部レイアウト方法がアップデートされました。
cell,sectionという概念にgroupというものが追加されて複雑な画面が構成できるようになりました。

こういった↓複雑な画面を、ちゃんと1個のUICollectionViewで表現できます。
UIStackView+ContainerView+UIScrollViewで複雑な画面を設計する

縦スクロールの中に横スクロールがあるからと言って、UICollectionViewを入れ子にする必要もありません

例:

Simulator Screen Shot - iPhone 12 - 2020-11-07 at 21.43.32.png

サンプルコードむずかしい😂

ここにサンプルコードがあるんですが、結構新しい概念が入ってきていて難しいです。
正直このレベルに複雑だとプロジェクトに導入するのを躊躇います
https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

なんで難しいかが分かった

どうやらこのサンプルコード
1.新しいレイアウトの実装方法
2.新しいDataSourceの実装方法
3.新しいCellの実装方法

の3つを使ってるみたいなんです。
3つの話が一気に入ってくるから分かりづらい。
このうち、2と3を無視して1だけに集中したら結構わかりやすかったので紹介します。

ちなみに 2.新しいDataSrourceの話

※読み飛ばしていいです

iOS13から導入されたNSDiffableDataSourceです。
https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot

これはUICollectionViewDatasourceの代わりとなるような仕組みです。
Sectionをstructで表現して、そのsectionとCellに対応するデータモデルをセットにして指定することで、どのcellにどのデータが入るかというのが自明になり、うれしいみたいみたいな、多分そんな仕組みだと思います。
MVVMみたいなことをしたいんでしょうね、気持ちはわかります。
特に複雑なレイアウトにした場合、どのセルにどのデータを該当させればいいかが分からなくなるので、必須なのかもしれません。
ただ個人的な感想ですが指定するデータがHashableじゃなきゃダメらしく面倒くさいし、全体的に前より見通しが悪くなってる感じがします。Cellに対応するデータが必ず1個とも限りませんし、正直私は使いたくありません。

ちなみに 3.新しいCellの実装方法

※読み飛ばしていいです

iOS14から導入されたCellRegistrationです。
https://developer.apple.com/documentation/uikit/uicollectionview/cellregistration

これもCellとDataModelをセットにして扱いたいというモチベーションだと思います。
cellをdequeueする際にはdequeueConfiguredReusableCellという見慣れないfunctionを使います。
https://developer.apple.com/documentation/uikit/uicollectionview/3600945-dequeueconfiguredreusablecell

こちらも個人的にはまだとっつきにくさがあります。あとiOS14からなのでほとんどのプロジェクトでは使えません。

「新しいレイアウト」を実装する際にこれまでと違うところ

主にこれだけです。

collectionView.collectionViewLayout = createLayout()

collectionViewLayout: UICollectionViewLayoutはiOS6からあったもので、これに新しいレイアウトを代入するだけで縦スクロール内横スクロールみたいなレイアウトを実現できます。簡単じゃん!

新しいレイアウトの作り方

これは色んな記事を探してもらったほうが早い気がします。
これとか
時代の変化に応じて進化するCollectionView ~Compositional LayoutsとDiffable Data Sources~

一応超簡単に解説しますと
・groupという概念が増えました
・groupには1個のcellが入るか、複数のcellを該当させることができます
・section単位で縦スクロール内で横スクロールにすることが可能
・ページングも可能
・複数のサイズの違うセルをグループ化できる

こんな感じ?

サンプル画像のレイアウト部分はこのような感じです。
そこそこ複雑ですが、これだけで複数のUICollectionViewを使わなくていいとか、複数のUIViewControllerを使わなくていいのはかなりいいと思います。

func createLayout() -> UICollectionViewLayout {

    return UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
        let sectionType = self.sections[sectionIndex]

        // ① cellのsizeを決める
        // ② layoutItemを作成する
        //    layoutItem(cell)のcontentInsetを決める(任意)
        // ③ groupのsizeを決める
        // ④ layoutItemからlayoutGroupを作る(様々な形式がある)
        //    layoutGroupのcontentInsetを決める(任意)
        // ⑤ layoutGroupからlayoutSectionを作る
        //    layoutSectionのcontentInsetを設定する(任意)
        //    layoutSection内のスペースを設定する(任意)
        //    layoutSection内でページングするかどうかを設定する(任意)
        //    layoutSectionを横スクロールにする(任意)
        // ⑥ return layoutSection

        switch sectionType {
        case .first:    // 1個のCell
            // ①
            let cellSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),    // 横幅いっぱい
                                                  heightDimension: .absolute(100.0))        // 固定100.0
            // ②
             let item = NSCollectionLayoutItem(layoutSize: cellSize)
            // ③
            let groupSize = cellSize    // cell1個だから全く同じ
            // ④
            let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // 横
            // ⑤
            let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
            return layoutSection

        case .second:   // 横スクロール
            // ①
            let cellSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), // グループに対して横いっぱい
                                                  heightDimension: .absolute(60.0)) // 固定60.0
            // ②
            let item = NSCollectionLayoutItem(layoutSize: cellSize)
            item.contentInsets = .zero
            // ③
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3),   // 横幅の30%
                                                   heightDimension: .absolute(60.0))        // 固定60.0
            // ④
            let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])   // 横, 1Group1Cell
            // ⑤
            let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
            // 横スクロールにする
            layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
            layoutSection.interGroupSpacing = 10
            layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 0, bottom: 10, trailing: 0)
            return layoutSection
        case .thiard:   // 上が横3列,下が横2列
            // 上①
            let cellSize1 = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0/2.0), // groupに対し1/3
                                                   heightDimension: .fractionalHeight(1.0))   // groupに対していっぱい
            // 上②
            let item1 = NSCollectionLayoutItem(layoutSize: cellSize1)
            // 上③
            let groupSize1 = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),   // 親groupに対していっぱい
                                                    heightDimension: .absolute(60.0))         // 固定60.0
            // 上④
            let layoutGroup1 = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize1,
                                                                  subitem: item1, count: 2)
            layoutGroup1.interItemSpacing = NSCollectionLayoutSpacing.fixed(2)
            // 下①
            let cellSize2 = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0/3.0), // groupに対し1/2
                                                   heightDimension: .fractionalHeight(1.0))   // groupに対していっぱい
            // 下②
            let item2 = NSCollectionLayoutItem(layoutSize: cellSize2)
            // 下③
            let groupSize2 = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),   // 親groupに対していっぱい
                                                    heightDimension: .absolute(60.0))         // 固定60.0
            // 下④
            let layoutGroup2 = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize2,
                                                                  subitem: item2, count: 3)
            layoutGroup2.interItemSpacing = NSCollectionLayoutSpacing.fixed(2)

            // groupのgroup③
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),   // 画面に対していっぱい
                                                   heightDimension: .absolute(122.0))       // 固定120.0
            // groupのgroup④
            let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: groupSize,
                                                               subitems: [layoutGroup1, layoutGroup2])
            layoutGroup.interItemSpacing = NSCollectionLayoutSpacing.fixed(2)

            // ⑤
            let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
            layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)
            return layoutSection
        case .fourth:   // 縦スクロール(無限)
            // ①
            let cellSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0/4.0), // groupに対して1/4
                                                  heightDimension: .fractionalHeight(1.0))   // groupに対していっぱい
            // ②
            let item = NSCollectionLayoutItem(layoutSize: cellSize)
            item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2)
            // ③
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),           // 画面に対していっぱい
                                                   heightDimension: .fractionalWidth(1.6/4.0))      // 画面横に対して1.6倍
            // ④
            let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
            // ⑤
            let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
            return layoutSection
        }
    }
}


ソースコードはこちら
https://github.com/osanaikoutarou/EasyModernCollectionViewExamplee

設計の参考
ひたすら楽してUITableView

感想

・groupは入れ子にできるっぽい
・pagingが簡単で嬉しい(ちゃんとやるとすげー面倒)
・.fractionalWidthが便利
・多重階層になるから、どこのスペースがどこにあたるか、どこのinsetsがどこに反映されるかが分かりづらい、慣れが必要

おわりに

SwiftUIでもこの概念は共通っぽいので、たぶん必修に成るんだと思います。
新しいDataSourceは気に食わないですけど。

追記: その他TIPS, メモ

・Header,Footerも新しいレイアウトで指定する必要があるらしい
https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views
この Add Headers and Footers to Sections あたり

追記: 案件で3つ画面を作った感想

案外融通がきかない
特にセルの縦横比を固定したい場合に面倒なコードになります

9
8
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
8