iOS15以降では空セル挙動を切り替えるのにfillerRowHeightを使おう

iOS15の[UITableView](https://developer.apple.com/documentation/uikit/uitableview)では、ついに「空セルが表示されない」挙動がデフォルトになりました。以前は空セル表示がデフォルトだったため、それを消すために

```swift
tableView.tableFooterView = UIView(frame: .zero)
```

のようなワークアラウンド実装を行なった開発者の方は多いのではないかと思います。

しかし、iOS15の挙動変更は[tableFooterView](https://developer.apple.com/documentation/uikit/uitableview/1614976-tablefooterview)のデフォルト値が空ビューになったの**ではなく**、空セルの挙動が新しい[fillerRowHeight](https://developer.apple.com/documentation/uikit/uitableview/3801921-fillerrowheight)(filter: フィルター、ではなく、filler: フィラー)というプロパティによって管理されるようになり、そのデフォルト値が「空セルを表示しない」値になっている事により実現されています。

この記事ではドキュメントに記載のある「プロパティの設定による挙動の違い3パターン」をサンプルとともに解説します。

なお、ここまでは馴染みのある「空セル」と呼称しましたが、これ以降は公式ドキュメントの表現に従って「filler row」という名称を使っていきます。「空いている部分を埋める行」というニュアンスです。

# fillerRowHeightの3種類の挙動
前提として、**fillerRowは[UITableView.style](https://developer.apple.com/documentation/uikit/uitableview/1614913-style)が[.plain](https://developer.apple.com/documentation/uikit/uitableview/style/plain)の時でないと表示されません。**なので、スタイルが[UITableView.Style.grouped](https://developer.apple.com/documentation/uikit/uitableview/style/grouped)や[UITableView.Style.insetGrouped](https://developer.apple.com/documentation/uikit/uitableview/style/insetgrouped)の場合にはfillerRowHeightに何の値を入れても無視されることになります。

そのため、`fillerRowHeight`の設定を有効にしたい場合は、Storyboardの設定やコード初期化で`style: .plain`を渡すようにします。

その上で、`filterRowHeight`はその値によって大きく3種類の挙動をすることがドキュメントによって示されています。

> ・0.0を設定するとfiller rowsを表示しません。この挙動はiOS15以上ではデフォルトです。
・[automaticDimension](https://developer.apple.com/documentation/uikit/uitableview/1614961-automaticdimension)を設定すると、table viewの最後の行の高さに合わせて、automatic heightを使うfiller rowsを表示します。この挙動はiOS15未満でデフォルトです。
・任意の正の値を設定すると、指定した高さのfiller rowsを表示します。

> Apple inc., fillerRowHeight, Discussion, https://developer.apple.com/documentation/uikit/uitableview/3801921-fillerrowheight, viewed: 2022/02/19

引用部分の一行目と二行目見てもらうと分かりますが、デフォルト挙動が切り替わっていることも明示されています。

# 検証
上記の三種類の挙動を実際に動かしてみていきます。検証コードはPlaygroundで書かれていますので、お手元の環境でペーストしてお試しください。

<details><summary>fillerRowHeight.playground</summary><div>

```swift
//: A UIKit based Playground for presenting user interface
  
import UIKit
import PlaygroundSupport

class MyViewController : UIViewController, UITableViewDataSource {

    /*
     style: .plainは必須
     */
    let tableView = UITableView(frame: .zero, style: .plain)

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .white
        view.addSubview(tableView)

        // AutolayoutをサボりたいときにたまにresizingMaskを書きます
        tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self

        self.view = view
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }

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

        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        var contentConfiguration = cell.defaultContentConfiguration()
        contentConfiguration.text = indexPath.row.description
        cell.contentConfiguration = contentConfiguration
        return cell

    }

}

let myViewController = MyViewController()

// fillerRowHeight = 0.0
// 検証に必要な際にアンコメントしてください
//myViewController.tableView.fillerRowHeight = 0.0 // default

// fillerRowHeight = UITableView.automaticDimenssion
// 検証に必要な際にアンコメントしてください
//myViewController.tableView.fillerRowHeight = UITableView.automaticDimension

// fillerRowHeight = 任意の正の値
// 検証に必要な際にアンコメントしてください
myViewController.tableView.fillerRowHeight = 30

PlaygroundPage.current.liveView = myViewController
```

</div></details>

## fillerRowHeight = 0.0
iOS15以上のデフォルト挙動である値ですが、`filler rows`が表示されていません。

![スクリーンショット 2022-02-19 12.00.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/b862b949-0221-ea4f-6c0b-2349a7b05b59.png)

(スクリーンショットでは画像境界の識別性のためにborderの設定をしています)

## fillerRowHeight = UITableView.automaticDimenssion
iOS15未満のデフォルト挙動である値ですが、`filler rows`がテーブルの高さと同じだけ表示されています。行の高さは自動で決まっています。

![スクリーンショット 2022-02-19 12.01.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/a1ccd882-0c0f-4326-96d5-7b0fb3d86f7d.png)

## fillerRowHeight = 任意の正の値
ここでは値を`30`としました。

「`filler rows`が指定した高さ分続く」のではなく「`filler rows`はテーブルの高さと同じだけ表示されるが、1つ1つの行の高さを任意に変更できる」という振る舞いです。

![スクリーンショット 2022-02-19 12.02.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/45631bae-9653-6db5-2683-48b3431777c1.png)

# まとめ
この記事では、我々がこれまで`empty rows`, `empty cells`などと読んでいたUITableView.Style.plainの際に表示されるあれをiOS15以降では`fillerRowHeight`というプロパティで設定できるようになったことを解説しました。

また、`fillerRowHeight`に設定した`CGFloat`の値によって実際にどうUITableViewの振る舞いが変わるのかをサンプルコードとスクリーンショットを使って例示しました。

# 参考
fillerRowHeight
https://developer.apple.com/documentation/uikit/uitableview/3801921-fillerrowheight

Apple Developer Forumでの質問
https://developer.apple.com/forums/thread/691284