[AutoLayout] Intrinsic Content Sizeを活用しよう〜その2 Self Sizing編〜


はじめに

こんにちは!Life is Tech ! #2 Advent Calendar 2020の22日目を担当しますふみっちです。

この記事は〜その1 概要編〜の続きとなっていて、Intrinsic Content Sizeに関する具体的な実装を解説を行います!
プロジェクト全体はこちらからダウンロードできるので是非手元で動かしてみてください😎


Intrinsic Content Sizeを用いると動的なコンテンツの変化によるサイズ調整を自動で実装できるようになるという話を〜その1 概要編〜に書かせてもらいましたが、おそらく動的なサイズ調整が必要になるケースとしてはUITableViewUICollectionViewを使用するが多いと思います。

そこで今回はUITableViewのCellをIntrinsic Content Sizeを用いてセルごとに可変なものにしていきます!

UITableViewCell × Intrinsic Content Size

TableViewやCollectionViewには一つ一つのセルの高さを可変にするという仕組みが備わっており、Self Sizingと呼ばれています。
このSelf Sizingは先ほど説明したIntrinsic Content Sizeを用いることで手動で計算をしなくても実現することができます。

Self Sizingの具体例

今回はSelf Sizingを使用して画像の大きさに合わせて動的な高さを持つUITableViewCellを作成する例を紹介します。
また、セルのボタンをタップするとコンテンツの表示・非表示を変更できる方法も紹介します。


Apple純正の「リマインダー」アプリでみるようなセルの表示・非表示もAppleの場合はセルの数自体を変えているように見えますが、リマインダーアプリに近いことができるようになると思います。

リマインダー

先ほどのリンクのSample2のプロジェクトに対応する内容です。

Demo

このように可変サイズを持つUIViewはIntrinsic Content Sizeをうまく使うことで簡単に実現できます!

実装ポイント

Cellの内部はIntrinsic Content Sizeを持つUIViewのみを配置する

Self Sizingによって自動でセルの高さを決定する場合はIntrinsic Content Sizeが有効である必要があります。なのでただのUIViewやUIScrollViewをセルの内部に配置すると正しくセルがリサイズされない場合があります。

セルを可変にする際はセルに配置するUIViewに注意

表示中のCellの高さを変更したい場合は一度セルをリロードする

Intrinsic Content Sizeを用いて手軽にCellごとの高さを動的に変更する際に注意点が一つ必要です。

それは一度表示したあとにCellの高さを変更したい場合はセルを再度更新する必要があるということです。(他にも方法はありそうですがこの方法が一番簡単だったので紹介します。)

ただ、UITableViewが持つreloadData()は全てのセルを更新してしまうので、同じくUITableViewが持つreloadRows(at:with:)というメソッドを用いて高さの変更が必要なセルのみを更新します。
このとき、ボタンがタップされたことはセル側が検知するため、ViewController側にデリゲートを利用して処理を移譲する必要があります。

デリゲートを定義してViewControllerに処理を渡す

以下のようにセルからViewControllerに処理を移譲するためにプロトコルを作成してあげます。

TableViewCell.swift
protocol TableViewCellDelegate: AnyObject {
    func didTapChangeVisibleButton(cell: TableViewCell)
}

このプロトコルにはdidTapChangeVisibleButtonというメソッドを定義していますが、引数にセル自身を渡してあげられるようなメソッドであれば他の名前でも大丈夫です!

ボタンタップ時にデリゲートメソッドを呼ぶ

次に上のTableViewCellDelegateを使用してセルでボタンがタップされたことを受け取ったら、ViewController(デリゲート)側に処理を伝えます。

TableViewCell.swift

import UIKit

class TableViewCell: UITableViewCell {
    @IBOutlet weak var randomImageView: UIImageView!

    // デリゲートをプロパティとして参照する    
    weak var delegate: TableViewCellDelegate?

    // ボタンがタップされたことをdelegate側に伝える
    @IBAction func tapChangeVisibleButton() {
        // 先ほど定義したメソッドを呼んでデリゲート側で処理をしてもらう
        delegate?.didTapChangeVisibleButton(cell: self)
    }
}

ViewController(デリゲート)側でセルを更新する

次にViewControllerが先ほど定義したTableViewCellDelegateに準拠し、セルの更新処理を行います。

ViewController.swift
import UIKit

extension ViewController: TableViewCellDelegate {
    func didTapChangeVisibleButton(cell: TableViewCell) {
        if let indexPath = tableView.indexPath(for: cell) {
            data[indexPath.row].toggle()
            tableView.reloadRows(at: [indexPath], with: .automatic)
        }
    }
}

先ほど紹介したtableView.reloadRows(at:with:)を呼ぶためにセルのIndexPathを取得する必要がありますが、引数にセル自身を渡しているのでtableView.indexPath(for: cell)という具合に取得することができます!

// 引数に指定したセルのIndexPathが取得できる
let indexPath: IndexPath? = tableView.indexPath(for: cell)

セルに関するデータモデル

セル一つ一つに対して画像を表示するのか・しないのかに関してはセル側ではなくViewController側で管理する必要があります。

今回は「セルに画像を表示しますか?」「はい、いいえ」という知識を持っていれば良いのでvar data: [Bool] = []というようにデータを定義します。

var data: [Bool] = [
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false,
    false
]

最初は画像を非表示の状態から始めたいのでfalseとしています。

UITableViewDataSource

ViewController側で実装されている以下の二つのメソッドの中身を解説していきます。

  • tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
  • tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int

このメソッドはセルの個数を決めるメソッドです。

配列dataの要素数と同じにすればデータの数だけセルが表示されるようになります。(セルの数を増やしたい場合は配列の要素を増やしましょう!)

tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

このメソッドはIndexPathというUITableViewにおけるセルの所在地を元に表示するセルを決定するメソッドです。

ここのメソッドの内部では以下のコードがミソです。

let isVisible = data[indexPath.row]
if isVisible {
    cell.randomImageView.image = imageArray[indexPath.row % 4]
} else {
    cell.randomImageView.image = nil
}

上記の最初の行では、data変数のセルに該当する要素を取得しています。

もしセルは画像を表示するべき(isVisibletrue)であればセルに画像を表示します。

一方セルは画像を表示するべきではない(isVisiblefalse)であればセルも画像はnilになります。

UIImageViewのIntrinsic Content Sizeimageプロパティの値によって変わるため、imageをnilにすると自動的にUIImageViewの高さも0になり、画像が代入されるとその画像に適する高さにリサイズされます。

このようにして自分で高さの調整をせずとも表示するデータに応じてセルが自動的にサイズを調整してくれるわけです!

完成イメージ

最後に

この記事では〜その1 概要編〜で説明したIntrinsic Content Sizeを交えた実装例を紹介しました。
Intrinsic Content Sizeはマイナーな内容かもしれないですが、実は普段から意識しなくても使っているということもあると思うのでこの機会に興味を持ってもらえると嬉しいです!

最後まで読んでいただきありがとうございました‼️