traitCollection.horizontalSizeClassのcompactとregularで別々のappearanceをサイドバーに適応させたいようなケースがあります。FinderやMusicなど、Apple純正Appではよく見られます。
このようなインターフェースはiOS HIGでもSidebarパターンとして言及されています。
Apply the correct appearance to a sidebar. To create a sidebar, use the sidebar appearance of a collection view list layout. For developer guidance, see UICollectionLayoutListConfiguration.Appearance.1
ガイドラインで実装方法について指定があるため、SidebarはUICollectionLayoutListConfigurationとUICollectionViewCompositionalLayoutを利用してUICollectionViewで実装します。
よく見られるUICollectionLayoutListConfigurationのサンプルは、以下のようにUICollectionViewCompositionalLayout.list(using:)を利用してレイアウトを作成しているものです。
let configration = UICollectionLayoutListConfiguration(appearance: .sidebar)
let layout = UICollectionViewCompositionalLayout.list(using: configration)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
これではlayout
が静的に決まってしまうので、動的にAppearanceを切り替えるには別のAPIを利用する必要があります。ここで、UICollectionViewCompositionalLayout.init(sectionProvider:)を利用します。
let layout = UICollectionViewCompositionalLayout { [unowned self] section, layoutEnvironment in
let appearance: UICollectionLayoutListConfiguration.Appearance = {
// sidebarとして利用するviewcontrollerは常にhorizontalSizeClassが.compactで返されるため、
// 画面全体の情報が知りたい場合はsplitViewController.traitCollection、
// view.window?.windowScene?.traitCollection、もしくはUITraitCollection.currentなどを利用する必要がある。
if self.splitViewController?.traitCollection.horizontalSizeClass == .compact {
return .insetGrouped
} else {
return .sidebar
}
}()
let configration = UICollectionLayoutListConfiguration(appearance: appearance)
return NSCollectionLayoutSection.list(using: configration, layoutEnvironment: layoutEnvironment)
}
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
本来このAPIはセクションごとに別のNSCollectionLayoutSectionオブジェクトを生成して返すことを目的として設計し実装されていますが、上記のサンプルのようにtraitCollectionなどの状態変化に適応させる事にも利用できます。
しかし、これだけでは変更があったときにappearanceの更新を実行することができず、トリガーするための実装が必要になります。
diffableDataSource.apply(diffableDataSource.snapshot(), animatingDifferences: false)
UICollectionViewDiffableDataSourceを利用している場合は、上記のようにapply()で同じsnapshotを適応させることにより「表示しているデータは変更しないが見た目の変更だけトリガーする」ということを実現できます。(※個人的に、このような見た目の変化だけを目的としたapply()を空撃ちと呼んでいます)
この空撃ちをContainer View Controller側のtraitCollectionDidChange(_:)から呼び出せるような実装にしておくと、適切に変更を適応できます。
注意事項ですが、この空撃ちの実行であったとしてもapply(snapshot)
を実行するとリストのセレクションがクリアされるため、見た目を変えたいだけで選択状態は維持したいというケースであれば以下のように一時保存変数をうまく活用して、apply(snapshot)
後に再度適応させてあげてください。
let stashedIndexPathForSelectedRow = collectionView.indexPathsForSelectedItems?.first
diffableDataSource.apply(diffableDataSource.snapshot(), animatingDifferences: false)
collectionView.selectItem(at: stashedIndexPathForSelectedRow, animated: false, scrollPosition: [])
このような動的な実装をした場合はお使いのUICollectionView.CellRegistrationの中でもステート変化に対応してCellの見た目が変わるように実装をしておきましょう。
例えば以下の例では、contentConfiguration, backgroundConfiguration, accessoriesをtraitCollectionによって分けて設定しています。
let cellRegiration = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { [unowned self] cell, indexPath, item in
/*
これは実際にプロジェクトで使っているコードの切り抜きですが、sidebarとして表示できる時はsidebarとして最適な見た目で表示し
compactの場合はsingle columnなhierarchical navigationのルート階層として振る舞うのに最適な、通常のUITableViewっぽいスタイルを
提供しています。
*/
// コンテンツ設定
var contentConfiguration: UIListContentConfiguration = {
if self.splitViewController?.traitCollection.horizontalSizeClass == .compact {
return cell.defaultContentConfiguration()
} else {
return UIListContentConfiguration.accompaniedSidebarCell()
}
}()
contentConfiguration.image = ...
contentConfiguration.text = ...
cell.contentConfiguration = contentConfiguration
// 背景設定
if self.containerSplitViewController.traitCollection.horizontalSizeClass == .compact {
cell.accessories = [.disclosureIndicator()]
cell.backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
} else {
cell.accessories = []
cell.backgroundConfiguration = UIBackgroundConfiguration.listAccompaniedSidebarCell()
}
}
まとめ
この記事では、UICollectionViewCompositionalLayoutのクロージャを引数にとるイニシャライザをうまく活用することで、traitCollectionの変更や任意のきっかけに応じて動的にLayoutやAppearanceを変更できることを紹介しました。
参考情報
今回の記事は、こちらのAppleのサンプルコードをもとに記載しています。解説は、ImplementingModernCollectionViews>Modern Collection Views> ListAppearancesViewController.swift>L66~L75のcreateLayout()というコードがやっていることを噛み砕いたものです。
-
(Apple inc., Apply the correct appearance to a sidebar, Sidebars, https://developer.apple.com/design/human-interface-guidelines/ios/bars/sidebars/, viewed: 2021/08/11) ↩