DZNEmptyDataSetは2014年6月(執筆時点で6年前)からある、EmptyStateを表示するためのライブラリで、今も現役で利用されている素晴らしいライブラリです。
ですが、「ちょっとEmptyStateの表示したいだけなのにライブラリを入れるほどでもないし...」とか「SPMに寄せたいけど、DZNEmptydataSetはSPM対応のPRまだマージしてなくて、stableだとCocoaPodsかCarthageしか使えないし...」とか「Method Swizzlingが怖い...」などなど、自作を試みたい動機がいくつかあると思います。
そこで、簡単に自作する方法を紹介したいと思います。SwiftUIではif
で切り分けて表示するだけで、非常に簡単なため今回は扱いません。
実装: Delegate
まず、このようにEmptyStateで表示したいラベルやViewを用意します。今回はラベルのみです。
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
が最前面に存在している形になります。
navigationController?.view.addSubview(emptyLabel)
navigationController?.view.bringSubviewToFront(emptyLabel)
emptyLabel.equalToParent()
レイアウトを簡単にするために、いくつかextension
を追加しています。
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
の切り替えを行うと期待する挙動をします。
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
などで記述できます。例えば以下のような形です。
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
の実現ができました。