Ideal Reality

興味の赴くままに

iOS14のUIDatePickerのデザインをカスタマイズする

iOS14からUIDatePickerのデザインが新しくなり、使いやすくなりました。特にUIDatePickerStyle.compactは日付を押したらDatePickerが表示されるという実装がしやすくなってます。

ただ、DatePickerを閉じている際のデザインを標準では変更できず少し使いづらいので、見た目変えてみたいと思います。

Contents
スポンサーリンク

基本的な方針

デフォルトのデザインをいじるのは将来のアップデートで崩壊する可能性が高くコードも煩雑になります。そこで、今回は元々のViewを透明にして、上に独自のViewを被せる形にします。

元々のデザインは単純で、グレーの背景色とtintColorで表示される日付のラベルだけです。これらを見えなくするのですが、無造作にopacity = 0にしてしまうとタップが効かなくなってカレンダーを開けなくなってしまうので、再帰的にbackgroundColorが指定されているViewを探し出して隠します。

そして、これをlayoutSubviewsなど画面描画のタイミングに呼ばれるメソッドに入れ込んでやれば、見えないけどタップしたらカレンダーが出てくるViewができます。

class CustomDatePicker: UIDatePicker {
    
    override func layoutSubviews() {
        super.layoutSubviews()
        makeTransparent(view: self)
    }
    
    func makeTransparent(view: UIView) {
        if view.backgroundColor != nil {
            view.isHidden = true
        } else {
            for subview in view.subviews {
                customize(view: subview)
            }
        }
    }
}

カスタムビューを重ねる

僕の場合日付のフォーマットとフォントサイズを変更したかったので、今回はこの上にUILabelを重ねてみます。

といっても、やることは単純で、

  • UILabelを生成
  • willMove(toSuperview:)など、Viewが表示されるまでに呼ばれるメソッド内でaddSubviewを行う
  • layoutSubviews()frametextの更新

といった感じ。サブクラスでUIViewを追加する常套手段だと思います。

class CustomDatePicker: UIDatePicker {
    
    let label = UILabel()
    
    func updateLabel() {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "ja_JP")
        formatter.setLocalizedDateFormatFromTemplate("MMM d yyyy hh:mm")
        label.text = formatter.string(from: date)
    }
    
    override func willMove(toSuperview newSuperview: UIView?) {
        super.willMove(toSuperview: newSuperview)
        addSubview(label)
    }
    
    func makeTransparent(view: UIView) {
        if view === label { return }
        if view.backgroundColor != nil {
            view.isHidden = true
        } else {
            for subview in view.subviews {
                customize(view: subview)
            }
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        makeTransparent(view: self)
        label.frame = CGRect(origin: .zero, size: frame.size)
        updateLabel()
    }
}

ちなみに、makeTransparentlayoutSubviewsで呼ぶのは、didMoveToSuperview()で1回呼んだ程度じゃ透明になってくれなかったから。

でも、いくら再帰関数を使っているとはいえ、対象は小さなUIDatePickerの中だけなので、せいぜい8つ程度しかViewが存在せず、これでレイアウト処理が重くなるってことはないと思います。

あと、追加したUILabeltextの更新もlayoutSubviewsで行うのは、

  • dateプロパティのdidSetでは変更を検知できない
  • 手軽にoverrideして変更を検知できるメソッドがない
  • addTargetは使いたくない
  • カレンダーを閉じるときにlayoutSubviewsが呼ばれる

から。適当に使ってみた限りlayoutSubviewsの呼び出し回数はそんなに多くなかったからこれでいいんじゃない?

ただ、UIDatePickerStyle.compactでも常にインライン編集になるMacCatalyst(Apple SiliconでiOS版を動かす際も)だとこの方法は使えないので注意してくださいね。

スポンサーリンク

コメント

投稿されたコメントはありません

名前

メールアドレス(任意)

コメント

関連する投稿

[SwiftUI]List内のButtonやLinkのデザインをNavigationLinkっぽくする方法

[Swift]Dictionaryに順番を持たせたい

[iOS, Swift]で画面回転時のアニメーションを無効化する

NSTextFieldにadjustsFontSizeToFitWidthがないから自作する

[SwiftUI]Apple Watchで画面遷移から戻った際にアニメーションが崩壊するのを防ぐ

NO IMAGE

SwiftUIでUISwipeGestureRecognizerを使ってスワイプを検出する