DZNEmptyDataSetを使わずにEmptyStateを実装する

[DZNEmptyDataSet](https://github.com/dzenbot/DZNEmptyDataSet)は2014年6月(執筆時点で6年前)からある、EmptyStateを表示するためのライブラリで、今も現役で利用されている素晴らしいライブラリです。

ですが、「ちょっとEmptyStateの表示したいだけなのにライブラリを入れるほどでもないし...」とか「SPMに寄せたいけど、DZNEmptydataSetはSPM対応のPRまだマージしてなくて、stableだとCocoaPodsかCarthageしか使えないし...」とか「Method Swizzlingが怖い...」などなど、自作を試みたい動機がいくつかあると思います。

そこで、簡単に自作する方法を紹介したいと思います。SwiftUIでは`if`で切り分けて表示するだけで、非常に簡単なため今回は扱いません。

# 実装: Delegate
まず、このようにEmptyStateで表示したいラベルやViewを用意します。今回はラベルのみです。

```swift:YourViewController.swift

    let emptyLabel: UILabel = {
        let label = UILabel(frame: .zero)
        label.font = UIFont.preferredFont(forTextStyle: .title1)
        label.textColor = UIColor.secondaryLabel
        label.textAlignment = .center
        label.text = "Text that you want to display"
        label.isUserInteractionEnabled = false
        return label
    }()

```

そのEmptyState用のViewを、スクリーンの高さと等しいViewに対して追加します。その理由としては、ツールバーなどが下部に含まれていると画面中心より上部にEmptyStateのViewが表示されてしまい(centerが高さ分ズレるため)、ビジュアルに違和感が生じるためです。今回は`navigationController?.view`としました。(ご自分の環境に合わせて適切にViewの選択をお願いします)

この実装ではViewDebuggerを見ると、`emptyLabel`が最前面に存在している形になります。

```swift:YourViewController.swift
navigationController?.view.addSubview(emptyLabel)
navigationController?.view.bringSubviewToFront(emptyLabel)
        
emptyLabel.equalToParent()
```

レイアウトを簡単にするために、いくつか`extension`を追加しています。

```swift:Constraint.swift

extension UIView {
    
    @_functionBuilder
    public struct ConstrainsBuilder {
        static func buildBlock(_ constraints: NSLayoutConstraint...) -> [NSLayoutConstraint] {
            constraints
        }
    }
    
    public func makeConstraints(@ConstrainsBuilder builder: (UIView) -> [NSLayoutConstraint]) {
        translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate(builder(self))
    }
    
    /// - Precondition: The superView should not be nil.
    public func equalToParent() {
        precondition(superview != nil)
        
        self.makeConstraints {
            $0.leadingAnchor.constraint(equalTo: superview!.leadingAnchor)
            $0.trailingAnchor.constraint(equalTo: superview!.trailingAnchor)
            $0.topAnchor.constraint(equalTo: superview!.topAnchor)
            $0.bottomAnchor.constraint(equalTo: superview!.bottomAnchor)
        }
        
    }
    
}
```

あとはテーブルやコレクションのデータソースで出し分けをするのみです。今回はテーブルの実装を解説します。

データソースの変更の際に呼ばれる`tableView(_:numberOfRowsInSection:) -> Int`の内部で、その時返すデータ数に応じて、`isHidden`の切り替えを行うと期待する挙動をします。

```swift:YourViewController+DataSource.swift

extension YourViewController {

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let numberOfRows = yourItems.count ?? 0
        
        if numberOfRows == 0 {
            emptyLabel.isHidden = false
        } else {
            emptyLabel.isHidden = true
        }
        
        return numberOfRows
    }
}

```

# 実装: Combine
できる限り`numberOfRowsInSection`はデータ数だけを返したいので、`Combine`フレームワークが利用できる場合は、できる限りそちらの方が良いです。`Publisher`を使えば`viewDidLoad`などで記述できます。例えば以下のような形です。

```swift:Publisher.swift

struct DataSource: NSObject {
    @Published
    var items: [String]
}

class ViewController : UIViewController {
    var dataSource: DataSource = .init()

    override func viewDidLoad() {
        dataSource.$items
            .map({ !$0.isEmpty })
            .receive(on: RunLoop.main)
            .assign(to: \.isHidden, on: emptyLabel)
            .store(to: &cancellables)
    }

}
```

# まとめ
多少ソースコードに余分を含むことにはなりますが、ライブラリを使わなくてもデータ件数による切り替えを行うだけで`EmptyState`の実現ができました。