UIContentUnavailableConfigurationでStateに応じて画面を手軽に切り替える

## はじめに
WWDC23(2023 Apple Worldwide Developers Confere)でUIKitの新機能が色々発表され、その中でも今後よく使っていくであろうEmpty State(UIContentUnavailableConfiguration)について備忘録としてまとめました。

公式ドキュメント

https://developer.apple.com/documentation/uikit/uicontentunavailableconfiguration/
## Empty Stateの概要
### 機能概要
以下公式発表ではEmpty Stateとして紹介されていますが、UIContentUnavailableConfigurationというUIKitのストラクチャです。Stateに応じて画面に画像/ラベル/ボタンを表示・切り替えることができます。

[公式紹介ビデオ](https://developer.apple.com/videos/play/wwdc2023/10055/?time=469)

### 特性と使い方
ViewControllerのcontentUnavailableConfigurationに対し様々なUIContentUnavailableConfigurationのStateを設定することにより、画面に画像/ラベル/ボタンを表示したり切り替えたりすることができます。
画像等を非表示にするためには明示的にnilを設定する必要があります。

## 実装例
```swift
import UIKit

class SecondViewController: UIViewController {
    
    // Stateを用意
    enum UnavailableState {
        case normal
        case empty
        case loading
        case error
    }
    
    @IBOutlet weak var tableView: UITableView!
    
    var screenState: UnavailableState = .loading
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 使用するセルをテーブルビューに登録する
        self.tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "tableViewCell")
        
        // ステータスを指定
        self.setupUnavailableConfiguration(state: .normal)
    }
    
    private func setupUnavailableConfiguration(state: UnavailableState) {
        self.screenState = state
        
        switch state {
            
        case .normal:
            // 画像・テキスト・ボタンを非表示
            self.contentUnavailableConfiguration = nil
            
        case .empty:
            var config = UIContentUnavailableConfiguration.search()
            config.text = "No Data"
            config.secondaryText = "Please try again"
            var buttonConfig =  UIButton.Configuration.filled()
            buttonConfig.title = "Retry"
            config.button = buttonConfig
            config.buttonProperties.primaryAction = UIAction(handler: { _ in
                // button action
            })
            self.contentUnavailableConfiguration = config
            
        case .loading:
            var config = UIContentUnavailableConfiguration.loading()
            config.text = "Please wait..."
            config.textProperties.font = .boldSystemFont(ofSize: 18)
            self.contentUnavailableConfiguration = config
            
        case .error:
            var config = UIContentUnavailableConfiguration.empty()
            config.image = UIImage(systemName: "exclamationmark.triangle")
            config.imageProperties.tintColor = .red
            config.text = "Error"
            config.textProperties.color = .red
            self.contentUnavailableConfiguration = config
        }
    }
}

extension SecondViewController: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // 用意するセルの個数
        return self.screenState == .normal ? 50 : 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // テーブルビューの枠内に表示するセルを指定する
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell") as? TableViewCell else {
            return UITableViewCell()
        }
        
        // セルのレイアウト設定
        cell.setCellLabel(cellIndex: indexPath.row.description)
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // セル押下時の挙動
        let storyboard = UIStoryboard(name: "ThirdView", bundle: nil)
        guard let thirdViewController = storyboard.instantiateViewController(withIdentifier: "thirdView") as? ThirdViewController else {
            return
        }
        
        guard let cell = tableView.cellForRow(at: indexPath) as? TableViewCell else {
            return
        }
        
        thirdViewController.randomString = cell.cellLabel.text ?? ""
        self.navigationController?.pushViewController(thirdViewController, animated: true)
    }   
}
```

## 画面キャプチャ
State | 正常 | Loading | Empty | Error
----- | --- | ------- | ----- | -----
　　　　画面　　　　 | ![normal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/4ff9f349-06db-13d9-3384-e9cecd4569f2.png) | ![loading.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/ac8db122-72b7-67f8-8fd1-e3726b252085.png) | ![empty.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/f6f5de1c-232e-7703-677c-d2eece5b74c0.png) |  ![error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3342288/fbc9dad0-9c02-e9aa-a28a-0df8004ea70f.png)

## まとめ
UIContentUnavailableConfigurationを使用するとローディング中やエラーの場合などに専用のビューを作成する必要がないのでとても魅力的ですね。アプリ開発において必須というわけではなさそうですが、知っておいて損は無い技術だと感じました。
WWDCではいくつもの新しい項目が発表され、追いつけていないどころか1年周回遅れになりそうですが、空き時間にコツコツ勉強していきたいところです。