Custom UIContentConfiguration で楽にCell 管理

iOS14から使えるようになった、 UIContentConfiguration 、便利ですよね。 CollectionViewCellを作らずにCollectionViewを使えます。

UICollectionViewListCell のdefaultContentConfiguration

まずは、 UICollectionViewListCell の、 defaultContentConfiguration を使ってみましょう。

cellからdefaultContentConfigurationでConfigurationを取得し、それにStringやImageを渡すだけで要素が表示されます。

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! UICollectionViewListCell

        let info = sampleData[indexPath.row]

        var cellConfiguration = cell.defaultContentConfiguration()
        cellConfiguration.text = info.title
        cellConfiguration.secondaryText = info.subTitle
        cellConfiguration.image = UIImage(systemName: info.iconName)
        cell.contentConfiguration = cellConfiguration

        return cell
    }

f:id:toyship:20220216132616j:plain:w200
defaultContentConfiguration

コードの全体はこちら。 UICollectionViewListCell defaultContentConfiguration · GitHub

UICellAccessory

UICollectionViewListCellには、アクセサリーをつけることも簡単です。

システムのアクセサリーはcheckmarkやdeleteなどが用意されています。 一度に表示できるシステムアクセサリーは一つです。

        cell.accessories = [.checkmark()]
        cell.accessories = [.disclosureIndicator(displayed: .always, options: .init(isHidden: false, reservedLayoutWidth: nil, tintColor: UIColor.red))]
        cell.accessories = [.delete()]

カスタムアクセサリーも表示することができます。 カスタムアクセサリーは一度に複数表示することができます。 表示位置は、leading/trailing、 編集中だけ表示、常時表示の切り替えもできます。

        let customAccessory1 = UICellAccessory.CustomViewConfiguration(
          customView: UIImageView(image: UIImage(systemName: "bandage")),
          placement: .leading(displayed: .always))

こちらは、3つのカスタムアクセサリーと一つのシステムアクセサリーを表示した例。 バンドエイドマークとトレイマークがleading側につけたアクセサリー、自転車マークがtrailing側につけたアクセサリーです。それにくわえてcheckmarkのシステムアクセサリーをつけています。

f:id:toyship:20220216133558j:plain:w200
UICellAccessory

全体のコードはこちらです。

UICellAccessory · GitHub

updateHandler

iOS15からは、updateHandlerも用意されていて、より柔軟にConfigurationを設定することができます。

        cell.configurationUpdateHandler = { cell, state in
            var content = UIListContentConfiguration.cell().updated(for: state)
            content.text = info.title + "!"
            content.secondaryText = info.subTitle
            content.image = UIImage(systemName: info.iconName)
            if state.isDisabled {
                content.textProperties.color = .systemGray
            }
            cell.contentConfiguration = content
        }

Custom UIContentConfiguration

手軽に使える defaultContentConfiguration ですが、そもそもUICollectionViewListCellでしか使えないんですよね。UICollectionViewCellで同じようなことをしたいな……と思ったら、Custom UIContentConfigurationを作ってみましょう。

ここでは、画像を大きめに表示する LargeImageConfig という UIContentConfiguration と、それが描画するView としてLargeImageContentViewというViewを作っています。

LargeImageConfigには、このConfigurationで保持したいデータを定義します。

struct LargeImageConfig: UIContentConfiguration {

    var mainImage: UIImage? = nil
    var mainTitle: String = ""
    var subTitle: String = ""
    var detail: String = ""

    func makeContentView() -> UIView & UIContentView {
        return LargeImageContentView(configuration:self)
    }
    func updated(for state: UIConfigurationState) -> LargeImageConfig {
        return self
    }

}

LargeImageContentViewは、表示するView部分。 UILabelやUIImageViewなどを置いてみます。

class LargeImageContentView : UIView, UIContentView {

    private var mainImageView: UIImageView = UIImageView(frame: CGRect.zero)
    private var mainTitle: UILabel = UILabel()
    private var subTitle: UILabel = UILabel()
    private var detail: UILabel = UILabel()

    var configuration: UIContentConfiguration{
        didSet{
            updateConfig()
        }
    }

    init(configuration: UIContentConfiguration) {
        self.configuration = configuration
        super.init(frame:.zero)

        addSubview(mainImageView)
        mainImageView.frame = CGRect(x: 220, y: 0, width: 80, height: 80)
        mainImageView.layer.borderColor = UIColor.lightGray.cgColor
        mainImageView.layer.borderWidth = 1.0
        mainImageView.layer.cornerRadius = 5.0
        mainImageView.contentMode = .scaleAspectFit

        mainTitle.frame = CGRect(x: 0, y: 0, width: 300, height: 40)
        mainTitle.font = UIFont.boldSystemFont(ofSize: 30)
        addSubview(mainTitle)

        subTitle.frame = CGRect(x: 0, y: 40, width: 300, height: 20)
        subTitle.font = UIFont.systemFont(ofSize: 15)
        addSubview(subTitle)

        detail.frame = CGRect(x: 50, y: 60, width: 250, height: 80)
        detail.font = UIFont.systemFont(ofSize: 10)
        detail.numberOfLines = 0
        addSubview(detail)

        updateConfig()

    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func updateConfig(){

        if let conf = configuration as? LargeImageConfig {
            mainImageView.image = conf.mainImage
            mainTitle.text = conf.mainTitle
            subTitle.text = conf.subTitle
            detail.text = conf.detail

        }
    }

}

表示するとこうなります。

f:id:toyship:20220216165450j:plain:w200
Custom UIContentConfiguration

全体のコードはこちらです。 Custom UIContentConfiguration · GitHub

TVMediaItemContentConfiguration

iOSのUICollectionViewCellには画像付きのconfigurationには画像付きのものはありませんが、tvos の UICollectionViewCellには、TVMediaItemContentConfiguration.wideCell などの、画像つきのセルを表示できるconfigurationがデフォルトで用意されています。

tvosでは、画面に表示する要素が限定的なため、このようなConfigurationが用意されていますが、iOSでは画面の表示要素がアプリによって大きく異なるため、システムに画像つきConfigurationが用意されることはなさそうです。

まとめ

UIContentConfigurationを使うとUICollectionViewCellを独自に用意しなくてもUICollectionViewが使えます。 ContentViewやConfigurationを作る必要があるので、手間としてはそれほど変わらないと思う方もいるかもしれませんが、ContentViewやConfigurationは、Modelから独立したレイアウト要素として分離して定義できるので、全体の設計がかなりすっきりしてくると思います。

なお、CollectionViewには、ここであげたUIContentConfiguration以外にも便利な機能が増えています。まだキャッチアップできていない感じなら、こちらのビデオをみるとよいでしょう。 CollectionViewの今までの歴史から始めて、ざっと新しい機能について説明してくれます。 詳しいことはこっちのビデオをみるといいよ!というリンクで他の動画に誘導してくれるので、概要を理解することができます。 Advances in UICollectionView - WWDC20 - Videos - Apple Developer

UIContentConfigurationが含まれた詳細なサンプルコードはこちら。 Implementing Modern Collection Views https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views