13
6

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 3 years have passed since last update.

【UIKit】モダンなUICollectionViewを知りたい

Posted at

はじめに

公式で公開されているImplementing Modern Collection Viewsサンプルコードを見てできることの多さに驚き、最新のコレクションビューのAPIについて調べてみることにしました。

コレクションビューの進化

👇こちらのWWDC20のプレゼンで詳しく説明されていました

iOS12まで(私の理解していたコレクションビュー)

スクリーンショット 2022-02-28 16.10.35.png

iOS13からDataLayoutが変わってこうなった

スクリーンショット 2022-02-28 16.11.32.png
Diffable Data Sourceについて👇

Compositional Layoutについて👇

iOS14からはさらに進化した

スクリーンショット 2022-02-28 16.12.46.png

Section Snapshotsについて👇

List Configurationについて👇

List CellView Configurationについて👇

サンプルコードを読んでみる

まずはサンプルアプリを表示します

Modern Collection Views

  • Compositional Layout
  • Diffable Data Source
  • Lists
  • Outlines
  • Cell Configuratons
    の構成となっていますが、初期表示画面を見ていくだけでも理解が深まると思いましたので、初期表示画面のサンプルコードを見ていきます

初期表示画面のViewController

OutlineViewControllerとなっていますが、これは初期画面(テーブルビューの見た目のコレクションビュー)のもので、項目のOutlinesとは別です💡

OutlineViewController
class OutlineViewController: UIViewController {
    // ※1
    enum Section {
        case main
    }
    // ※1
    class OutlineItem: Hashable {
        let title: String
        let subitems: [OutlineItem]
        let outlineViewController: UIViewController.Type?

        init(title: String,
             viewController: UIViewController.Type? = nil,
             subitems: [OutlineItem] = []) {
            self.title = title
            self.subitems = subitems
            self.outlineViewController = viewController
        }
        func hash(into hasher: inout Hasher) {
            hasher.combine(identifier)
        }
        static func == (lhs: OutlineItem, rhs: OutlineItem) -> Bool {
            return lhs.identifier == rhs.identifier
        }
        private let identifier = UUID()
    }
    // ※1
    var dataSource: UICollectionViewDiffableDataSource<Section, OutlineItem>! = nil
    var outlineCollectionView: UICollectionView! = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Modern Collection Views"
        configureCollectionView()
        configureDataSource()
    }
    
    private lazy var menuItems: [OutlineItem] = {
        return [
            OutlineItem(title: "Compositional Layout", subitems: [
                OutlineItem(title: "Getting Started", subitems: [
                    OutlineItem(title: "Grid", viewController: GridViewController.self),
                    OutlineItem(title: "Inset Items Grid",
                                viewController: InsetItemsGridViewController.self),
                    OutlineItem(title: "Two-Column Grid", viewController: TwoColumnViewController.self),
                    OutlineItem(title: "Per-Section Layout", subitems: [
                        OutlineItem(title: "Distinct Sections",
                                    viewController: DistinctSectionsViewController.self),
                        OutlineItem(title: "Adaptive Sections",
                                    viewController: AdaptiveSectionsViewController.self)
                        ])
                    ]),
                OutlineItem(title: "Advanced Layouts", subitems: [
                    OutlineItem(title: "Supplementary Views", subitems: [
                        OutlineItem(title: "Item Badges",
                                    viewController: ItemBadgeSupplementaryViewController.self),
                        OutlineItem(title: "Section Headers/Footers",
                                    viewController: SectionHeadersFootersViewController.self),
                        OutlineItem(title: "Pinned Section Headers",
                                    viewController: PinnedSectionHeaderFooterViewController.self)
                        ]),
                    OutlineItem(title: "Section Background Decoration",
                                viewController: SectionDecorationViewController.self),
                    OutlineItem(title: "Nested Groups",
                                viewController: NestedGroupsViewController.self),
                    OutlineItem(title: "Orthogonal Sections", subitems: [
                        OutlineItem(title: "Orthogonal Sections",
                                    viewController: OrthogonalScrollingViewController.self),
                        OutlineItem(title: "Orthogonal Section Behaviors",
                                    viewController: OrthogonalScrollBehaviorViewController.self)
                        ])
                    ]),
                OutlineItem(title: "Conference App", subitems: [
                    OutlineItem(title: "Videos",
                                viewController: ConferenceVideoSessionsViewController.self),
                    OutlineItem(title: "News", viewController: ConferenceNewsFeedViewController.self)
                    ])
            ]),
            OutlineItem(title: "Diffable Data Source", subitems: [
                OutlineItem(title: "Mountains Search", viewController: MountainsViewController.self),
                OutlineItem(title: "Settings: Wi-Fi", viewController: WiFiSettingsViewController.self),
                OutlineItem(title: "Insertion Sort Visualization",
                            viewController: InsertionSortViewController.self),
                OutlineItem(title: "UITableView: Editing",
                            viewController: TableViewEditingViewController.self)
                ]),
            OutlineItem(title: "Lists", subitems: [
                OutlineItem(title: "Simple List", viewController: SimpleListViewController.self),
                OutlineItem(title: "Reorderable List", viewController: ReorderableListViewController.self),
                OutlineItem(title: "List Appearances", viewController: ListAppearancesViewController.self),
                OutlineItem(title: "List with Custom Cells", viewController: CustomCellListViewController.self)
            ]),
            OutlineItem(title: "Outlines", subitems: [
                OutlineItem(title: "Emoji Explorer", viewController: EmojiExplorerViewController.self),
                OutlineItem(title: "Emoji Explorer - List", viewController: EmojiExplorerListViewController.self)
            ]),
            OutlineItem(title: "Cell Configurations", subitems: [
                OutlineItem(title: "Custom Configurations", viewController: CustomConfigurationViewController.self)
            ])
        ]
    }()
    
}
※1 dataSourceを定義している部分について

dataSourceUICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject where SectionIdentifierType : Hashable, ItemIdentifierType : Hashableとなるため、パラメータの型が合うようにSectionとItemの型を定義する必要がある
ドキュメント:UICollectionViewDiffableDataSource

次にextension内を見ていきます

OutlineViewController
extension OutlineViewController {

    func configureCollectionView() {
        // ※2
        let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: generateLayout())
        view.addSubview(collectionView)
        collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        collectionView.backgroundColor = .systemGroupedBackground
        self.outlineCollectionView = collectionView
        collectionView.delegate = self
    }

    func configureDataSource() {
        // ※3
        let containerCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, OutlineItem> { (cell, indexPath, menuItem) in
            // Populate the cell with our item description.
            var contentConfiguration = cell.defaultContentConfiguration()
            contentConfiguration.text = menuItem.title
            contentConfiguration.textProperties.font = .preferredFont(forTextStyle: .headline)
            cell.contentConfiguration = contentConfiguration
            
            let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .header)
            cell.accessories = [.outlineDisclosure(options: disclosureOptions)]
            cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
        }
        // ※3
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, OutlineItem> { cell, indexPath, menuItem in
            // Populate the cell with our item description.
            var contentConfiguration = cell.defaultContentConfiguration()
            contentConfiguration.text = menuItem.title
            cell.contentConfiguration = contentConfiguration
            cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
        }
        // ※3
        dataSource = UICollectionViewDiffableDataSource<Section, OutlineItem>(collectionView: outlineCollectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, item: OutlineItem) -> UICollectionViewCell? in
            // Return the cell.
            if item.subitems.isEmpty {
                return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
            } else {
                return collectionView.dequeueConfiguredReusableCell(using: containerCellRegistration, for: indexPath, item: item)
            }
        }

        // load our initial data
        // ※4
        let snapshot = initialSnapshot()
        self.dataSource.apply(snapshot, to: .main, animatingDifferences: false)
    }
    // ※2
    func generateLayout() -> UICollectionViewLayout {
        let listConfiguration = UICollectionLayoutListConfiguration(appearance: .sidebar)
        let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
        return layout
    }
    // ※4
    func initialSnapshot() -> NSDiffableDataSourceSectionSnapshot<OutlineItem> {
        var snapshot = NSDiffableDataSourceSectionSnapshot<OutlineItem>()

        func addItems(_ menuItems: [OutlineItem], to parent: OutlineItem?) {
            snapshot.append(menuItems, to: parent)
            for menuItem in menuItems where !menuItem.subitems.isEmpty {
                addItems(menuItem.subitems, to: menuItem)
            }
        }
        
        addItems(menuItems, to: nil)
        return snapshot
    }

}
※2 コレクションビューのレイアウトを定義している部分について

コレクションビューのイニシャライズ時に使っているgenerateLayout()内の
UICollectionLayoutListConfiguration(appearance: .sidebar)
UICollectionViewCompositionalLayout.list(using: listConfiguration)
はリストセクションだけを含むコンポジションレイアウトを作成する際に使用する方法です
ドキュメント:UICollectionLayoutListConfiguration

UICollectionLayoutListConfiguration(appearance: .sidebar)は他にも👇のように設定可能

(appearance: .plain)

(appearance: .grouped)

(appearance: .insetGrouped)

(appearance: .sidebar)(比較のため)

(appearance: .sidebarPlain)

※3 セルを生成する部分について

セルの生成は

ドキュメント
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { cell, indexPath, item in
    
    var contentConfiguration = cell.defaultContentConfiguration()
    
    contentConfiguration.text = "\(item)"
    contentConfiguration.textProperties.color = .lightGray
    
    cell.contentConfiguration = contentConfiguration
}

を使います
今回のサンプルでは、親セルと子セルの二種類(containerCellRegistrationcellRegistration)を生成しています
ドキュメント:UICollectionView.CellRegistration

そして実際に描画する部分は

ドキュメント
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
    (collectionView: UICollectionView, indexPath: IndexPath, itemIdentifier: Int) -> UICollectionViewCell? in
    
    return collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
                                                        for: indexPath,
                                                        item: itemIdentifier)
}

これでCellを返します
ドキュメント:UICollectionViewDiffableDataSource

※4 セルに初期値を与える部分について

コレクションビューに初期値を与える方法は👇の通りNSDiffableDataSourceSectionSnapshotを生成してdataSource.applyする

ドキュメント
for section in Section.allCases {
    // Create a section snapshot
    var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<String>()
    
    // Populate the section snapshot
    sectionSnapshot.append(["Food", "Drinks"])
    sectionSnapshot.append(["🍏", "🍓", "🥐"], to: "Food")
    
    // Apply the section snapshot
    dataSource.apply(sectionSnapshot,
                     to: section,
                     animatingDifferences: true)
}

ドキュメント:NSDiffableDataSourceSectionSnapshot

最後にUICollectionViewDelegateextensionを見ていきます

OutlineViewController
extension OutlineViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let menuItem = self.dataSource.itemIdentifier(for: indexPath) else { return }
        
        collectionView.deselectItem(at: indexPath, animated: true)
        
        if let viewController = menuItem.outlineViewController {
            navigationController?.pushViewController(viewController.init(), animated: true)
        }     
    }
}

セルタップ時の処理はcollectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)と、お馴染みのものを使っている
ただし、メソッド内ではUICollectionViewDiffableDataSourcefunc itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType?menuItemを取得している

感想

まだまだサンプルコードを見ていかないと理解したとは言えませんが、初期表示画面の作りだけでも新しい要素がてんこ盛りで混乱しました。
引き続きModern Collection Viewsと向き合っていきたいと思います。

ちなみに

iOS15からはコレクションビューの描画パフォーマンスが向上しているとか

参考

13
6
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
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?