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

## はじめに

こんにちは！[Life is Tech ! #2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/life-is-tech-2)の22日目を担当しますふみっちです。

この記事は[〜その1 概要編〜](https://qiita.com/fummicc1_dev/private/64ee8dfef8a54df0f4dd)の続きとなっていて、`Intrinsic Content Size`に関する具体的な実装を解説を行います！
プロジェクト全体は[こちら](https://github.com/fummicc1-lit/AdventCalendar2020)からダウンロードできるので是非手元で動かしてみてください😎

---

`Intrinsic Content Size`を用いると動的なコンテンツの変化によるサイズ調整を自動で実装できるようになるという話を[〜その1 概要編〜](https://qiita.com/fummicc1_dev/private/64ee8dfef8a54df0f4dd)に書かせてもらいましたが、おそらく動的なサイズ調整が必要になるケースとしては`UITableView`や`UICollectionView`を使用するが多いと思います。  
そこで今回は`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の場合はセルの数自体を変えているように見えますが、リマインダーアプリに近いことができるようになると思います。

|リマインダー|
|---|
|![](https://media.giphy.com/media/mGWNCR6rgntp4DLFgp/giphy.gif)|

---

先ほどのリンクの[Sample2](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/App/Sample2)のプロジェクトに対応する内容です。

|Demo|
|---|
|![](https://media.giphy.com/media/6CaDXP4iSyCV5AXwbx/giphy.gif)|

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

## 実装ポイント

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

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

|セルを可変にする際はセルに配置するUIViewに注意|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/SubAssets/Sample3/include_only_valid_size.png?raw=true)|

### 表示中の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)`という具合に取得することができます！

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

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

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

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

```swift
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におけるセルの所在地を元に表示するセルを決定するメソッドです。  
ここのメソッドの内部では以下のコードがミソです。  

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

上記の最初の行では、`data`変数のセルに該当する要素を取得しています。  
もしセルは画像を表示するべき（`isVisible`が`true`）であればセルに画像を表示します。  
一方セルは画像を表示するべきではない（`isVisible`が`false`）であればセルも画像は`nil`になります。

UIImageViewの`Intrinsic Content Size`は`image`プロパティの値によって変わるため、`image`をnilにすると自動的にUIImageViewの高さも0になり、画像が代入されるとその画像に適する高さにリサイズされます。  
このようにして自分で高さの調整をせずとも表示するデータに応じてセルが自動的にサイズを調整してくれるわけです！

|完成イメージ|
|---|
|![](https://media.giphy.com/media/6CaDXP4iSyCV5AXwbx/giphy.gif)|


## 最後に

この記事では[〜その1 概要編〜](https://qiita.com/fummicc1_dev/private/64ee8dfef8a54df0f4dd)で説明した`Intrinsic Content Size`を交えた実装例を紹介しました。
`Intrinsic Content Size`はマイナーな内容かもしれないですが、実は普段から意識しなくても使っているということもあると思うのでこの機会に興味を持ってもらえると嬉しいです！  
最後まで読んでいただきありがとうございました‼️