[iOS] UITableViewにUIDatePickerをインライン表示する

本記事では、UITableViewにUIDatePickerをインライン表示する実装を紹介します。
2020.04.13

はじめに

こんにちは。CX事業本部の平屋です。

本記事では、以下のスクショのような、UITableViewUIDatePickerをインライン表示する実装を紹介します。

iOS標準のカレンダーアプリなどで採用されているUIで、UIDatePickerの真上のセルをタップすると、UIDatePickerの表示/非表示を切り替えられます。

検証環境

  • macOS Mojave 10.15.3
  • Xcode Version 11.3.1

実現方法

インラインでのUIDatePicker表示を実現する方法はいくつかありますが、本記事では、UILabelUIDatePickerを持つセルを用意し、このセルの高さを動的に変更することによって、インライン表示のUIDatePickerの表示/非表示を切り替えられるようにします。UIDatePickerの代わりに他のコンポーネント(UIPickerViewなど)を使う場合でも同様な動きが実現できるかと思います。

サンプルプロジェクト

本記事は大まかな実装のみ解説します。実装の詳細は、以下のリポジトリで公開しているサンプルプロジェクトを参照してください。

PickerCellのレイアウトを作成する

まず、UILabelUIDatePickerを持つPickerCellのレイアウトを組んでいきます。

PickerCellのContentViewのサブビューにContainerView(UIView)を追加し、さらにそのサブビューにUILabel, UIDatePickerを追加します。ビューの階層は以下のようになります。

  • PickerCell
    • ContentView
      • ContainerView(UIView)
        • UILabel
        • UIDatePicker

ContainerViewに制約を追加する

ContainerViewの4辺(top, bottom, leading, trailing)に関する制約と、ContainerViewの高さの制約を追加します。(高さの制約の値は260、後述するUILabelUIDatePickerの高さの和が260なので260を設定)

UILabelに制約を追加する

UILabelの4辺(top, bottom, leading, trailing)に関する制約と、UILabelの高さの制約を追加します。(高さの制約の値は44)

UIDatePickerに制約を追加する

UIDatePickerの2辺(leading, trailing)に関する制約と、UIDatePickerの高さの制約を追加します。(高さの制約の値は216)

UILabelのbottomとUIDatePickerとの間の制約は追加しましたが、UIDatePickerのbottomに関する制約は追加していません。後述の実装によってContainerViewの高さをUILabelの高さと等しくすると、UIDatePickerを隠すことができます。

PickerCellを実装する

続いてPickerCellにコードを追加していきます。

アウトレットを繋ぐ

ContainerViewの高さの制約、UIDatePickerUILabelPickerCellにアウトレット接続します。

import UIKit

class PickerCell: UITableViewCell {
    @IBOutlet weak var containerViewHeight: NSLayoutConstraint!
    @IBOutlet weak var datePicker: UIDatePicker!
    @IBOutlet weak var label: UILabel!

    // ...
}

PickerCell生成時の処理を実装する

UIDatePickerUILabelのセットアップを行う実装を追加します。

Pickerはデフォルトで非表示にしたいのでPickerを非表示にし、ContainerViewの高さの制約をPicker非表示時用の値(compressedHeight)にします。

class PickerCell: UITableViewCell {
    // ...
    override func awakeFromNib() {
        super.awakeFromNib()
        prepare()
    }

    @objc func datePickerValueDidChange(sender: UIDatePicker) {
        label.text = PickerCell.formatter.string(from: sender.date)
    }

    // ...
}

private extension PickerCell {
    static let compressedHeight: CGFloat = 44
    static let expandedHeight: CGFloat = 260

    // ...

    static let formatter: DateFormatter = // ...

    func prepare() {
        // Picker非表示時用の値を設定
        containerViewHeight.constant = PickerCell.compressedHeight

        let now = Date()
        datePicker.date = now
        datePicker.addTarget(self, action: #selector(datePickerValueDidChange), for: .valueChanged)

        // Pickerを非表示にする
        datePicker.isHidden = true
        datePicker.alpha = 0

        label.text = PickerCell.formatter.string(from: now)
    }
}

Pickerを表示する処理を実装する

ContainerViewの高さの制約をPicker表示時用の値(expandedHeight)に変更し、animate(withDuration:animations:completion:)を呼ぶ実装を追加します。

class PickerCell: UITableViewCell {
    // ...

    func showPicker() {
        guard datePicker.isHidden else { return }

        // セルの高さの制約の値を変更して、Pickerが見えるようにする
        containerViewHeight.constant = PickerCell.expandedHeight
        datePicker.isHidden = false
        UIView.animate(withDuration: 0.25) {
            self.datePicker.alpha = 1
            self.layoutIfNeeded()
        }
    }

    // ...
}

Pickerを非表示にする処理を実装する

非表示にする処理も同様に実装します。

class PickerCell: UITableViewCell {
    // ...

    func hidePicker() {
        guard !datePicker.isHidden else { return }

        // セルの高さの制約の値を変更して、Pickerが隠れるようにする
        containerViewHeight.constant = PickerCell.compressedHeight
        UIView.animate(withDuration: 0.25, animations: {
            self.datePicker.alpha = 0
            self.layoutIfNeeded()
        }, completion: { _ in
            self.datePicker.isHidden = true
        })
    }

    // ...
}

ViewControllerを実装する

UITableViewDataSourceUITableViewDelegateのメソッドを実装します。

PickerCellがタップされたら、performBatchUpdates(_:completion:)を呼び、第一引数のブロック内でPickerCellhidePicker()またはshowPicker()を呼びます。

import UIKit

class ViewController: UITableViewController {
    private var showingDatePicker = false
    private var pickerCell: PickerCell?

    // ...
}

extension ViewController {
    override func numberOfSections(in tableView: UITableView) -> Int {
        // ...
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // ...
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // ...
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        guard indexPath.section == 1 else { return }

        // PickerCellを更新する
        tableView.performBatchUpdates({
            // Pickerを表示する(非表示にする)メソッドを呼ぶ
            if self.showingDatePicker {
                pickerCell?.hidePicker()
            } else {
                pickerCell?.showPicker()
            }
        }, completion: { _ in
            self.showingDatePicker.toggle()
        })
    }

    // ...
}

動作確認

Pickerはデフォルト非表示で、PickerCellをタップすると表示/非表示がアニメーション付きで切り替わります。

さいごに

本記事では、UITableViewにインラインでUIDatePickerを表示する実装を紹介しました。同じような実装をやろうとしている方の参考になれば幸いです。

サンプルプロジェクトは以下のリポジトリで公開していますのでこちらも参考にしてください。

参考資料