[UITableView] 反転処理を行わずにセルを下から順に描画する

# 経緯

* TableViewのセルは通常上から順に追加されていきますが、下から積み上げていくように描画したい場合があるかと思います。
* そんな時に、よくある解決としては`tableView.transform = CGAffineTransformMake(1, 0, 0, -1, 0, 0)`としてTableViewを反転表示させ、その後`cell.transform = CGAffineTransformMake(1, 0, 0, -1, 0, 0)`としてセルの向きを反転するというものがあります。
* それでもいいけど、、、今回は反転処理を行わずに、セルを下から描画していきたいと思います。

# 本記事の実装する仕様

* 画面の構成要素はボタン、テーブルビュー、ラベルの３つ。
* テーブルビューの最下部には、予めデフォルトのセルが１つ有り、これは削除できないものとする。
* ボタンを押すとデフォルトのセルの上に、下から順にセルが追加描画される。
* ボタンを押して追加されたセルの数によって、ラベルのアルファ値が変動する。

# UIの配置

* 上から成功ラベル(UILabel)、努力テーブルビュー(UITableView)、UITableViewCell、やる気ボタン(UIButton)を適当に配置して、ViewControllerとOutlet接続しておきます。
![スクリーンショット 2023-02-04 23.54.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2470935/48e705a8-da36-7fbd-ee65-c39aaecf4aba.png)

```Swift
    // MARK: - Outlets
    @IBOutlet weak var successLabel: UILabel! // 成功ラベル
    @IBOutlet weak var effortTableView: UITableView! // 努力テーブルビュー
    @IBOutlet weak var motivationButton: UIButton! // やる気ボタン
```

* やる気ボタンはIBActionでも接続しましょう。
```Swift
    @IBAction func tappedMotivationButton(_ sender: Any) {
        effortCount += 1
        effortTableView.reloadData()
    }
```
* やる気ボタンを押すと努力カウントが加算されて、努力テーブルビューが更新されます。
* effortCount: 努力カウントについては後述

# UIのセットアップ

* UIの表示に関する処理をまとめて今回はviewDidApear内で実行するようにします。（TableViewのframeを取得する必要があるため）

```Swift
    private func setupUI() {
        successLabel.alpha = 1.0 / (effortTableView.frame.height / cellHeight)
        successLabel.layer.cornerRadius = 10 // 角丸
        successLabel.clipsToBounds = true // 角丸適用
        
        effortTableView.delegate = self
        effortTableView.dataSource = self
        effortTableView.isScrollEnabled = false // スクロール不可
        effortTableView.separatorStyle = .none // 罫線非表示
        
        motivationButton.layer.cornerRadius = 10 // 角丸
        motivationButton.layer.borderColor = UIColor.red.cgColor // 枠線色
        motivationButton.layer.borderWidth = 2 // 枠線幅
    }
```

```Swift
    override func viewDidAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // セルの個数を算出
        cellCount = Int(effortTableView.frame.height / cellHeight)
        setupUI()
    }
```

※cellCount: 今回はAutoLayoutを意識して動的に対応できるように、effortTableViewの高さを取得してセルの高さで割ることで、セルの個数を算出しています。

# プロパティを定義

```Swift
    // MARK: - Properties
    // 努力カウント
    private var effortCount: Int = 0 {
        // effortCountが更新されるたびに呼ばれる
        didSet {
            guard let cellCount = cellCount else { return }
            // 努力カウントの値で成功ラベルの透明度を変更
            self.successLabel.alpha = 1.0 / (CGFloat(cellCount) / CGFloat(effortCount))
            // 努力カウントがセルの個数（才能セルの分の1を引いた個数）に並んだら成功ラベルのテキストを変更
            if effortCount == Int((cellCount) - 1) {
                successLabel.text = "🌟大成功🌟"
                successLabel.textColor = .black
            } else {
                successLabel.text = "成功"
                successLabel.textColor = .white
            }
            // 努力値カウントの下限は0
            if effortCount < 0  {
                effortCount = 0
            }
            // 努力カウントの上限はセルの個数-1
            if effortCount >= cellCount {
                effortCount = cellCount - 1
            }
        }
    }
    private let cellHeight: CGFloat = 30 // セルの高さ
    private var cellCount: Int? // セルの個数（viewDidAppear時に算出）
```

* やる気ボタンを押すと、努力カウントが加算されて、努力テーブルビューのセルと成功ラベルが描画されていきます

# UITableViewの実装

* ようやく本題ですね。
* 反転処理を使わずにセルを下から描画するには、tableViewのサイズ内に描画できるセルの個数と描画したい要素の個数の差を空のセルで埋めることで実現できます。
* 最下セルをデフォルトセルとして特定の振る舞いを実装するには、indexPath.row == (セルの個数-1)として特定することで実現できます。
* 以上を踏まえて、以下コード。

```Swift
extension ViewController: UITableViewDataSource, UITableViewDelegate {
    // セルの数
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let cellCount = cellCount else { return 1 }
        return cellCount
    }
    
    // セルの描画
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        guard let cellCount = cellCount else { return UITableViewCell()}
        
        // セルの個数-1番目のセルが一番下のセル
        if indexPath.row == cellCount - 1 {
            cell.textLabel?.text = "✨才能✨"
        } else {
            // 一番下のセルから努力の数を引いた数番目のセルは空のセル
            if indexPath.row < cellCount - 1 - effortCount {
                cell.textLabel?.text = ""
            } else {
                // それ以外は努力のセル
                cell.textLabel?.text = "💢努力💢"
            }
        }
        cell.textLabel?.textAlignment = .center // テキストを中央揃えで表示
        cell.selectionStyle = .none // セルタップ時に選択状態にならないように設定
        return cell
    }
    
    // セルの高さ
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return cellHeight
    }
    
    // スワイプ操作によるセルの削除
    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        guard let cellCount = cellCount else { return nil }
        
        // 空のセルは削除できないようにする
        if indexPath.row < cellCount - 1 - effortCount {
            return nil
        }
        // 才能セルは削除できないようにする
        if indexPath.row == cellCount - 1 {
            return nil
        }
        // 削除ダイアログ表示
        let action = UIContextualAction(style: .destructive, title: "サボる😜") { [weak self] _, _, completion in
            self?.showAlert()
            completion(true)
        }
        action.backgroundColor = .red
        let configuration = UISwipeActionsConfiguration(actions: [action])
        return configuration
    }
    
    func showAlert() {
        let alert = UIAlertController(title: "確認",
                                      message: "サボっていいの？😥",
                                      preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "サボる😜", style: .default, handler: { _ in
            // 努力カウントを減算してテーブルビューを更新
            self.effortCount -= 1
            self.effortTableView.reloadData()
        }))
        alert.addAction(UIAlertAction(title: "やっぱり頑張る😤", style: .cancel, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }
}
```

# 完成

![成功.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2470935/390fada4-a96a-80e9-aeb5-0eddb8fe3d6f.gif)

* やる気を押すと、才能の上に努力が積み重ねられ、ぼんやりとした成功が明瞭化していく、、、そしてサボると成功から遠ざかる、という動きが実装できました。

# あとがき

* こんな具合で反転処理を使わずとも頑張って下から描画することは可能です。
* 描画したいデータや画面仕様によっては選択肢として持っておいてもいいのかな？反転のほうがラクかもだけど。。。


<details>

<summary> 全コード </summary>

```Swift
import UIKit

class ViewController: UIViewController {
    
    // MARK: - Properties
    // 努力カウント
    private var effortCount: Int = 0 {
        // effortCountが更新されるたびに呼ばれる
        didSet {
            guard let cellCount = cellCount else { return }
            // 努力カウントの値で成功ラベルの透明度を変更
            self.successLabel.alpha = 1.0 / (CGFloat(cellCount) / CGFloat(effortCount))
            // 努力カウントがセルの個数（才能セルの分の1を引いた個数）に並んだら成功ラベルのテキストを変更
            if effortCount == Int((cellCount) - 1) {
                successLabel.text = "🌟大成功🌟"
                successLabel.textColor = .black
            } else {
                successLabel.text = "成功"
                successLabel.textColor = .white
            }
            // 努力値カウントの下限は0
            if effortCount < 0  {
                effortCount = 0
            }
            // 努力カウントの上限はセルの個数-1
            if effortCount >= cellCount {
                effortCount = cellCount - 1
            }
        }
    }
    private let cellHeight: CGFloat = 30 // セルの高さ
    private var cellCount: Int? // セルの個数（viewDidAppear時に算出）
    
    // MARK: - Outlets
    @IBOutlet weak var successLabel: UILabel! // 成功ラベル
    @IBOutlet weak var effortTableView: UITableView! // 努力テーブルビュー
    @IBOutlet weak var motivationButton: UIButton! // やる気ボタン
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // セルの個数を算出
        cellCount = Int(effortTableView.frame.height / cellHeight)
        setupUI()
    }
    
    private func setupUI() {
        successLabel.alpha = 1.0 / (effortTableView.frame.height / cellHeight)
        successLabel.layer.cornerRadius = 10 // 角丸
        successLabel.clipsToBounds = true // 角丸適用
        
        
        effortTableView.delegate = self
        effortTableView.dataSource = self
        effortTableView.isScrollEnabled = false // スクロール不可
        effortTableView.separatorStyle = .none // 罫線非表示
        
        motivationButton.layer.cornerRadius = 10 // 角丸
        motivationButton.layer.borderColor = UIColor.red.cgColor // 枠線色
        motivationButton.layer.borderWidth = 2 // 枠線幅
    }
    
    @IBAction func tappedMotivationButton(_ sender: Any) {
        effortCount += 1
        effortTableView.reloadData()
    }
}

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    // セルの数
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let cellCount = cellCount else { return 1 }
        return cellCount
    }
    
    // セルの描画
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        guard let cellCount = cellCount else { return UITableViewCell()}
        
        // セルの個数-1番目のセルが一番下のセル
        if indexPath.row == cellCount - 1 {
            cell.textLabel?.text = "✨才能✨"
        } else {
            // 一番下のセルから努力の数を引いた数番目のセルは空のセル
            if indexPath.row < cellCount - 1 - effortCount {
                cell.textLabel?.text = ""
            } else {
                // それ以外は努力のセル
                cell.textLabel?.text = "💢努力💢"
            }
        }
        cell.textLabel?.textAlignment = .center // テキストを中央揃えで表示
        cell.selectionStyle = .none // セルタップ時に選択状態にならないように設定
        return cell
    }
    
    // セルの高さ
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return cellHeight
    }
    
    // スワイプ操作によるセルの削除
    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        guard let cellCount = cellCount else { return nil }
        
        // 空のセルは削除できないようにする
        if indexPath.row < cellCount - 1 - effortCount {
            return nil
        }
        // 才能セルは削除できないようにする
        if indexPath.row == cellCount - 1 {
            return nil
        }
        // 削除ダイアログ表示
        let action = UIContextualAction(style: .destructive, title: "サボる😜") { [weak self] _, _, completion in
            self?.showAlert()
            completion(true)
        }
        action.backgroundColor = .red
        let configuration = UISwipeActionsConfiguration(actions: [action])
        return configuration
    }
    
    func showAlert() {
        let alert = UIAlertController(title: "確認",
                                      message: "サボっていいの？😥",
                                      preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "サボる😜", style: .default, handler: { _ in
            // 努力カウントを減算してテーブルビューを更新
            self.effortCount -= 1
            self.effortTableView.reloadData()
        }))
        alert.addAction(UIAlertAction(title: "やっぱり頑張る😤", style: .cancel, handler: nil))
        self.present(alert, animated: true, completion: nil)
    }
}

```

</details>