0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-16

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

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

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

実装: Delegate

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

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が最前面に存在している形になります。

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

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

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の切り替えを行うと期待する挙動をします。

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などで記述できます。例えば以下のような形です。

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の実現ができました。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?