Yappli で iOS エンジニアをしている三宅です!

Yappli では CMS を利用してアプリをノーコードで作成できるアプリプラットフォームの開発を行っています。そんなプラットフォームは動的に UI を構築する特性から複雑なレイアウトを組み立てる処理がいくつもあります。

複雑なレイアウトを組み立てるのに一部の機能では Compositional Layout を採用して実装している箇所がありますが、DecorationView を設定するときにも CMS から取得した Appearance 情報を動的に適用したくなります。

CollectionView の DecorationView は SupplementaryView のように参照を取得することができなさそうなので、工夫しつつ実現可能か試してみたいと思います。

アプローチ

DecorationView は UICollectionReusableView を継承して実装し、 apply(_ layoutAttributes:) メソッドを override してカスタムした UICollectionViewLayoutAttributes を受け取るようにしてみます。

カスタムの UICollectionViewLayoutAttributes は、UICollectionViewCompositionalLayout を継承したクラスを定義して layoutAttributesForElements(rect:) で設定する形で実装をしていこうと思います💪🏻

UICollectionViewLayoutAttribute を継承する

セクションを用意して書くほどの内容ではないですが、Appearance 情報を持つ UICollectionViewLayoutAttribute を定義していきます。

final class DecoratableLayoutAttributes: UICollectionViewLayoutAttributes {
  private var appearance: Appearance?
}

UICollectionView Compositional Layout を継承する

次に UICollectionViewCompositionalLayout を継承して layoutAttributesForElements(rect:) メソッドを override し、 DecoratableLayoutAttributes を設定していきます。

final class DecoratableCompositionalLayout: UICollectionViewCompositionalLayout {
  private let appearances: [Appearance]
  init(appearances: [Appearance]) {
    self.appearances = appearances
  } 
  ...
  override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let layoutAttributes = super.layoutAttributesForElements(in: rect)
    return layoutAttributes?.map { [weak self] attribute in
      var decoratableAttribute = DecoratableLayoutAttributes()
      decoratableAttribute.appearance = self?.appearances[attribute.indexPath.section]
      ...
      return decoratableAttribute
    }
  }
}

apply メソッドで Attribute の参照を得る

DecorationView で apply(_ layoutAttributes:) を override して DecoratableLayoutAttributes の参照を取得します。

final class BlockBackgroundDecoration: UICollectionReusableView {
  ...
  override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
    if let attribute = layoutAttributes as? DecoratableLayoutAttributes,
       let appearance = attribute.appearance {
      // ここで受け取った Apperance を DecorationView へ適用する
    }
  }
}

Section と UICollectionView に DecorationView の設定をする

NSCollectionLayoutSection には DecorationView を設定する decorationItems というプロパティがあります。このプロパティに NSCollectionLayoutDecorationItem を設定することで、指定した elementKind の DecorationView を利用してくれます。

let section = NSCollectionLayoutSection(group: group)
section.decorationItems = [
  .background(elementKind: elementKind)
]

また UICollectionView にも利用する DecorationView のクラスと elementKind を登録しておきます。