こんにちは、レシピ事業部でiOSエンジニアをしている山田(@0x746572616e79)です。
iOS 26で導入されたLiquid Glassは、iOS 27で強制的に有効化される予定です。対応を先送りにすると、その間に進む機能開発やデザイン調整がすべてLiquid Glassへの考慮がされないまま積み上がり、後から手戻りが増えていきます。早期に有効化しておくことで、新しい画面や機能を追加する段階でLiquid Glassを前提としたデザイン議論やベストプラクティスの調査ができるようになります。
こうした背景からクックパッドiOSアプリでは早めの対応を進めてきました。この記事では、UINavigationBarとUITabBar周りで遭遇した破壊的変化と、Liquid Glassの新しい仕組みを活用した事例を共有します。
UINavigationBar
カスタムtitleViewのUITextFieldがRTLでクラッシュする
まずはUINavigationBarに関わる問題です。
クックパッドアプリではいくつかの画面でnavigationItem.titleViewにカスタムの検索バーを設定していました。Liquid Glassを有効化してデバッグしている中で、モーダル遷移かつRTL表示の組み合わせでクラッシュすることに気づきました。ナビゲーションバーの内部レイアウトが刷新された影響でUISearchBarTextFieldのレイアウト計算が破綻し、CALayerのpositionにNaNが入ることが原因でした。
このクラッシュはiOS 26.2では再現されますがiOS 26.4では修正された模様ですが、クックパッドアプリはiOS17以降をサポートするためそのまま利用することはできません。
titleViewベースの検索バーをやめて、UISearchControllerベースに移行することで解決しました。
navigationItem.searchControllerとnavigationItem.preferredSearchBarPlacement = .stackedを用いたレイアウトであればiOS 26.2でも問題なく動作します。
// Before navigationItem.titleView = customSearchBar // After let searchController = UISearchController(searchResultsController: nil) searchController.searchBar.delegate = self navigationItem.searchController = searchController navigationItem.preferredSearchBarPlacement = .stacked
UISearchControllerへ移行することでLiquid Glassのガラス質感や検索バーのアニメーションも自然に適用されます。
ナビゲーションバーのアイテムが省略されてしまう
Liquid Glassではナビゲーションバーのバーボタンアイテムの扱いが大きく変わりました。従来はleftBarButtonItemsとrightBarButtonItemsの描画サイズが自動調整され、コンテンツの要求するサイズが長い場合でも他要素の表示領域を確保した上でいっぱいに広げることができました。クックパッドのレシピ詳細画面ではこの仕様を利用してレシピタイトルを左寄せにしていましたが、Liquid Glass有効下では各アイテムに最低44ptのタップ領域と8pt程度のスペースが確保されるようになり、収まらないアイテムは標準のoverflowボタンにまとめられるようになりました。

これにより表示可能領域よりも長いタイトルで全ての要素が標準のOverflowボタンに省略されてしまいました。
タイトルは、そもそもBarButtonItemsに入れること自体が少々特殊な対応だったためレイアウトの調整を頑張るのではなく、中央寄せになる挙動を許容しnavigationItem.titleViewへ移行することにしました。
また、これまでrightBarButtonItemsに自前でUIMenuを表示するOverflowボタンを表示していましたが、UIKitが表示する標準のoverflowボタンの中に意図せず省略されてしまうといった問題が起きやすくなるため、標準の仕組みへ乗ることにしました。具体的にはnavigationItem.additionalOverflowItemsにメニュー項目を登録して置くことで、自動的に標準のOverflowボタンを利用することができます。
navigationItem.additionalOverflowItems = UIDeferredMenuElement.uncached { completion in completion([ UIAction(title: "Delete", attributes: .destructive) { _ in /* ... */ } ]) }
一点注意が必要なのが、UIBarButtonItem(customView:)で初期化したアイテムの扱いです。titleやimageで初期化した場合はUIKitが自動的にmenuRepresentationを設定してくれますが、customViewで初期化した場合はmenuRepresentationがnilになります。menuRepresentationがnilの場合、表示領域が足りなくなるとOverflowメニューにもナビゲーションバーにも表示されなくなります。
let item2 = UIButton() item2.setTitle("Item2", for: .normal) navigationItem.rightBarButtonItems = [ UIBarButtonItem(title: "Item1", style: .plain, target: nil, action: nil), // Item2だけcustomView UIBarButtonItem(customView: item2), UIBarButtonItem(title: "Item3", style: .plain, target: nil, action: nil), UIBarButtonItem(title: "Item4", style: .plain, target: nil, action: nil), UIBarButtonItem(title: "Item5", style: .plain, target: nil, action: nil), UIBarButtonItem(title: "Item6", style: .plain, target: nil, action: nil) ]

customViewベースのアイテムを使う場合はmenuRepresentationを明示的に設定する必要があります。
let barButtonItem = UIBarButtonItem(customView: button) barButtonItem.menuRepresentation = UIAction(title: "Item2") { _ in /* ... */ }

UITabBar
FABと「今日作る」ボタンをUITabBarに統合する
従来のクックパッドアプリでは、レシピ作成用のFAB(Floating Action Button)と今日の献立を開く「今日作る」ボタンをUITabBarの上にオーバーレイとして配置していました。タブバーだけLiquid Glassのデザインに切り替わった状態で、その上に従来のオーバーレイが乗っているとデザインの統一性がなく違和感があったため、Liquid Glassで追加された新しい仕組みを使ってタブバーに統合することにしました。
FABはUITabBarItem(tabBarSystemItem: .search)を使ってタブバー右端にピン留めし、「今日作る」ボタンはUITabAccessoryとしてタブバー下部に配置することで、オーバーレイを廃止してコンテンツの表示領域を広げました。


Liquid GlassのUITabBarでは、tabBarSystemItem: .searchで作成したアイテムが他のタブとは独立してタブバーの右端に固定配置されます。本来は検索タブ用途のシステムアイテムですが、この固定配置の仕様を利用して、FABのようなアクションボタンをタブバーに統合しています。
献立アクセサリはUITabAccessoryで実現しています。Apple Musicのミニプレーヤーが代表例ですが、クックパッドでは今日の献立に登録されたレシピのサムネイルとカレンダーボタンを常時表示し、どの画面からでも献立にアクセスできるようにしています。
tabBarController.tabBarMinimizeBehavior = .onScrollDown tabBarController.setBottomAccessory( UITabAccessory(contentView: accessoryContentView), animated: false )
従来のオーバーレイボタンでは自前で表示領域を確保する都合上、コンテンツの邪魔にならないようラベルとアイコンのみの表示に留めていましたがUITabAccessoryではUIKit側が表示領域を管理してくれるため、今日作るレシピのサムネイルを表示するといった表示内容の自由度が高まりました。
コンテンツビューはtraitCollection.tabAccessoryEnvironmentで.inlineと.regularが切り替わるので、それぞれに合わせてラベルの表示有無やサムネイルの表示枚数を調整しています。
override func updateProperties() { super.updateProperties() switch traitCollection.tabAccessoryEnvironment { case .inline: // コンパクト表示に切り替え case .regular, .unspecified: // 通常表示に切り替え } }


hidesBottomBarWhenPushedとの連携

デバッグ中に、hidesBottomBarWhenPushed = trueの画面に遷移した際、タブバーは隠れるのにアクセサリが残り続ける問題に気づきました。これはUINavigationControllerDelegateのwillShowでアクセサリの保存・復元を手動で行うことで解決しました。
func navigationController(_ nc: UINavigationController, willShow vc: UIViewController, animated: Bool) { if vc.hidesBottomBarWhenPushed { storedAccessory = tabBarController?.bottomAccessory tabBarController?.setBottomAccessory(nil, animated: false) } else if let stored = storedAccessory { tabBarController?.setBottomAccessory(stored, animated: false) storedAccessory = nil } }
インタラクティブなpopジェスチャーがキャンセルされた場合の復元もtransitionCoordinatorのキャンセルハンドラで行っています。
contentScrollViewのコンテナVC転送
Liquid GlassのタブバーはtabBarMinimizeBehavior = .onScrollDownを設定することで、コンテンツのスクロールに連動してbottomAccessoryがタブバーの領域に収まるよう折りたたまれます。UIKitはcontentScrollView(for:)で返されるUIScrollViewを監視してこの挙動を実現しており、UIViewControllerを直接タブに配置している場合は自動で解決されます。
ただし、ViewControllerを入れ子にしていたりUIPageViewControllerを間に挟んでいる場合は注意が必要です。tabBarMinimizeBehaviorを設定しても反応する画面としない画面があり、特定のページでしかスクロール連動が機能しないなど意図しない挙動になることがあります。クックパッドでもカスタムのコンテナVCが子VCへ適切に転送できていない問題に遭遇し、contentScrollView(for:)のoverrideで対応しました。
override func contentScrollView(for edge: NSDirectionalRectEdge) -> UIScrollView? { children.first?.contentScrollView(for: edge) }
まとめ
Liquid Glassはカスタム実装をOS標準パターンに置き換えること、追加された新しい仕組みを活用することの2軸を柱に対応を進めました。
標準APIに準拠するほど対応コストは下がります。UISearchControllerやoverflowメニューへの移行はその好例です。また、UITabAccessoryやtabBarSystemItem: .searchの固定配置といった新しい仕組みを活用することで、FABや献立ボタンをタブバーに統合しコンテンツの表示領域を広げることもできました。
一方ですべてを標準UIに寄せればいいわけではなく、サービスらしさを表現するためにカスタムが必要な箇所もあります。どこを標準に乗せてどこをこだわるか、そのバランスを意識しながら対応を進めていくことが大切です。特にUITabAccessoryはミニプレーヤー以外の用途での情報がまだ少なく、この記事が参考になれば幸いです。
