Liquid Glass Offという方針をとる場合の現実的な落とし所 — パーツ単位で妥協点を探った話

システム開発第二事業部の冨永です。

主にiOS/iPadアプリの開発を担当しております。

今回は、Liquid Glassをパーツ単位でOFFにする現実的な落とし所について書きたいと思います。

Liquid GlassをすべてOFFにするのは意外と難しい

Xcode 26 / iOS 26から、新しいUIデザインであるLiquid Glassが導入されました。

Xcode 26でビルドしたアプリをiOS 26上で動かすと、タブバー、ナビゲーションバー、ツールバーなどの標準コントロールにLiquid Glassが自動的に適用されます。

AppleとしてはLiquid Glassへの準拠を推奨しており、基本的には標準のデザインを受け入れるのが最も低コストです。

補足:Liquid Glassは、iOS 26から導入された、半透明のガラスのような質感を持つ新しいUIデザインです。背景の色やコンテンツを透過させたり、ぼかしながら表示し、ナビゲーションバーやボタンなどの標準UIに自動的に適用されます。

iOS 26以降のLiquid Glassでは流体ガラスのような滑らかなデザインが標準パーツに適用されます。

iOS 18以前ではフラットなデザインでした。

ただし、実際の開発では、デザイン調整に割ける工数や既存画面との整合性など、さまざまな事情によりLiquid Glassを極力OFFにしたいケースもあるかと思います。

とはいえ、すべてのLiquid Glassを消そうとすると、局所的な対応が必要なため、工数との相談になります。

本記事では、実際の案件で検討した内容をもとに、以下について整理します。

  • どのパーツのLiquid GlassをOFFにしたか
  • どのパーツは対応を見送ったか
  • 対応をView Extensionとして共通化した方法
  • SwiftLintとAIエージェントのRulesによって実装漏れを防いだ方法

前提:Info.plistのフラグとの違い

Info.plistUIDesignRequiresCompatibilityを設定することで、iOS 26でもiOS18以前のUIに近い互換モードを利用できます。

ただし、Appleのドキュメントでは、この設定は新しいデザインへの対応を進める間に一時的に使用するものとされており、iOS 27では無効になると明言されています。

一方で、本記事で紹介する対応は、基本的にInfo.plistのフラグには依存していません。

そのため、互換モードが利用できなくなった後も、パーツ単位の対応として引き続き機能する見込みです。

ただし、Appleが推奨する方向とは逆の対応になるため、OSアップデートのたびに挙動を確認するコストが発生します。

恒久的な解決策というよりも、既存デザインを維持する必要がある場合の現実的な対応として考えて、お読みいただければ幸いです。

方針と背景

今回の方針はシンプルです。

工数と相談し、対応可能なパーツだけ、Glassエフェクトのない実装へ置き換える

すべてのパーツを一律でOFFにするのではなく、次の観点からパーツごとに対応の可否を判断しました。

  • 実装コスト
  • 今後の保守コスト
  • 既存デザインへの影響
  • ユーザー操作への影響
  • OSバージョンごとの差異

対応した際の背景として、Liquid Glassに準拠したデザイン調整を行うための工数を十分に確保できない事情がありました。

そこで今回は、新しいデザインへ全面的に合わせるのではなく、可能な範囲で既存のデザインに近い見た目を維持する方向を選択しました。

各パーツで対応した内容を整理していきます。

対応したこと

ToolbarContentに対してsharedBackgroundVisibility(.hidden)を設定することで、ナビゲーションバー上のボタンに付与されるGlass背景を非表示にできます。

.toolbar {
    ToolbarItem(placement: .topBarTrailing) {
        Button("設定") {
            // 設定画面を表示する
        }
    }
    .sharedBackgroundVisibility(.hidden)
}

注意点として、Glassエフェクトを非表示にした場合、ナビゲーションの縦幅が長くなり、下部に余白が生まれます。

細かなサイズ調整が難しい箇所でもあるため、デザイン確認時に認識しておく必要があります。

NavigationBarのボタン「<」の下に若干の余白が残る......

カスタム戻るボタンを独自に実装する

sharedBackgroundVisibility(.hidden)では、システムが表示する戻るボタンのGlassエフェクトをOFFにできません。

そのため、戻るボタンについてはシステムのボタンを非表示にし、ToolbarItemを使って独自のボタンへ置き換えます。

struct DetailView: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        Text("詳細画面")
            .navigationBarBackButtonHidden(true) // 標準の戻るボタンを非表示
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    Button {
                        dismiss()
                    } label: {
                        Image(systemName: "chevron.backward")
                    }
                }
                .sharedBackgroundVisibility(.hidden)
            }
    }
}

この対応には、主に3つの注意点があります。

  1. sharedBackgroundVisibilityではシステムの戻るボタンを制御できないため、独自実装が必要になる
  2. fullScreenCoversheet内で独自の戻るボタンを使用している場合、修正漏れが起きやすい
  3. システムの戻るボタンを非表示にすると、画面端からのスワイプバックが使えなくなる

特に3点目は重要です。

画面上のボタンから戻る操作はできますが、標準の戻るボタンが持っていたインタラクティブなスワイプバック操作は失われます。

見た目だけではなく、操作性への影響も含めて判断する必要があります。

ToolbarのLiquid GlassをOFFにする

Toolbarについても、NavigationBarのボタンと同様にsharedBackgroundVisibility(.hidden)を使用します。

.toolbar {
    ToolbarItemGroup(placement: .topBarTrailing) {
        Button {
            // 編集処理
        } label: {
            Image(systemName: "pencil")
        }

        Button {
            // 削除処理
        } label: {
            Image(systemName: "trash")
        }
    }
    .sharedBackgroundVisibility(.hidden)
}

画面ごとに同じ指定を繰り返すことになるため、後ほど紹介するView Extensionとして共通化しました。

スクロールエッジのLiquid GlassをOFFにする

スクロール可能なViewにscrollEdgeEffectStyle(.hard, for: [.all])を付与することで、スクロール時のエッジに適用されるLiquid Glassの表現を抑制できます。

ScrollView {
    VStack {
        // コンテンツ
    }
}
.scrollEdgeEffectStyle(.hard, for: [.all])

iOS 26では、コンテンツがナビゲーションバーの下を通過した際、ナビゲーションバー周辺にスクロールエッジのエフェクトが適用されます。

.hardを指定すると、より不透明で境界の明確な見た目となり、スクロールに伴う色の変化を抑制できます。

View Extensionとして共通化する

ここまで紹介した実装を画面ごとに直接記述すると、次のような問題が発生します。

  • 各画面に#availableの判定が増える
  • .sharedBackgroundVisibility(.hidden)の付け忘れが起きる
  • 通常遷移とモーダル表示で実装が混在する
  • 将来仕様が変わった際に、複数の画面を修正する必要がある

そこで、Liquid GlassをOFFにする処理をView Extensionとしてまとめました。

今回用意したExtensionは、次の4つです。

Extension 用途
flatBackButton() NavigationLinkなどで遷移した画面のシステム戻るボタンを置き換える
flatCustomBackButton(action:) sheetfullScreenCoverなど、システム戻るボタンが存在しない画面で使用する
flatScrollEdgeEffect() スクロールエッジのLiquid Glass表現を抑制する
flatToolbar(content:) NavigationBar上のToolbarボタンをフラットな見た目にする

通常の画面遷移用:flatBackButton

NavigationLinkなどでプッシュ遷移した画面では、flatBackButton()を使用します。

//
//  View+FlatBackButtonModifier.swift
//

import SwiftUI

#if os(iOS)
extension View {
    /// iOS 26のLiquid Glassピルを除去したフラットな戻るボタンを適用する
    ///
    /// システムの戻るボタンを非表示にし、
    /// Liquid Glassのないカスタム戻るボタンに差し替える。
    /// NavigationLink遷移先のViewに付与すること。
    ///
    /// - Note: iOS 18以前では何もしない。
    ///   システムの戻るボタンがそのまま使用される。
    public func flatBackButton() -> some View {
        modifier(FlatBackButtonModifier())
    }
}

/// NavigationBarの戻るボタンのLiquid Glass除去用ViewModifier
private struct FlatBackButtonModifier: ViewModifier {
    @Environment(\.dismiss) private var dismiss

    func body(content: Content) -> some View {
        if #available(iOS 26.0, *) {
            content
                .navigationBarBackButtonHidden(true)
                .toolbar {
                    ToolbarItem(placement: .topBarLeading) {
                        Button {
                            dismiss()
                        } label: {
                            Image(systemName: "chevron.backward")
                        }
                    }
                    .sharedBackgroundVisibility(.hidden)
                }
        } else {
            content
        }
    }
}
#endif

利用する側では、遷移先のViewにModifierを付けるだけです。

struct DetailView: View {
    var body: some View {
        ScrollView {
            Text("詳細画面")
        }
        .navigationTitle("詳細")
        .flatBackButton()
    }
}

iOS 26以降ではカスタム戻るボタンへ置き換わり、iOS 18以前ではシステムの戻るボタンがそのまま使用されます。

利用側がOSバージョンを意識しなくてよい点がメリットです。

なお、flatBackButton()を使用しても、カスタム戻るボタンへの置き換えによって失われるスワイプバック操作は復元されません。

このExtensionは実装を共通化するものであり、システムの戻る操作を完全に再現するものではない点には注意が必要です。

モーダル表示用:flatCustomBackButton

sheetfullScreenCoverでは、通常のプッシュ遷移と異なり、システムの戻るボタンが存在しないケースがあります。

そのような画面では、iOSのバージョンにかかわらずカスタムボタンを表示するflatCustomBackButton()を使用します。

//
//  View+FlatCustomBackButtonModifier.swift
//

import SwiftUI

#if os(iOS)
extension View {
    /// Liquid Glassのないフラットなカスタム戻るボタンを常に表示する
    ///
    /// fullScreenCoverやsheet内のNavigationViewなど、
    /// システムの戻るボタンが存在しない場面で使用する。
    ///
    /// - Parameter action:
    ///   ボタンタップ時のカスタムアクション。
    ///   nilの場合はdismiss()を実行する。
    public func flatCustomBackButton(
        action: (() -> Void)? = nil
    ) -> some View {
        modifier(FlatCustomBackButtonModifier(action: action))
    }
}

/// fullScreenCoverやsheet内で、
/// Liquid Glassのないカスタム戻るボタンを表示するViewModifier
private struct FlatCustomBackButtonModifier: ViewModifier {
    @Environment(\.dismiss) private var dismiss

    var action: (() -> Void)?

    func body(content: Content) -> some View {
        if #available(iOS 26.0, *) {
            content
                .navigationBarBackButtonHidden(true)
                .toolbar {
                    ToolbarItem(placement: .topBarLeading) {
                        Button {
                            if let action {
                                action()
                            } else {
                                dismiss()
                            }
                        } label: {
                            Image(systemName: "chevron.backward")
                        }
                    }
                    .sharedBackgroundVisibility(.hidden)
                }
        } else {
            content
                .navigationBarBackButtonHidden(true)
                .toolbar {
                    ToolbarItem(placement: .topBarLeading) {
                        Button {
                            if let action {
                                action()
                            } else {
                                dismiss()
                            }
                        } label: {
                            Image(systemName: "chevron.backward")
                        }
                    }
                }
        }
    }
}
#endif

sheet内で画面を閉じるボタンとして使用する場合は、次のようになります。

struct ModalView: View {
    var body: some View {
        NavigationStack {
            Text("モーダル画面")
                .navigationTitle("設定")
                .flatCustomBackButton()
        }
    }
}

引数を省略した場合は、内部でdismiss()が呼び出されます。

一方、WebViewなどで独自の戻る処理が必要な場合は、actionを指定できます。

struct WebViewScreen: View {
    var body: some View {
        WebView()
            .flatCustomBackButton {
                // WebViewの履歴を戻る
            }
    }
}

通常の画面遷移用とモーダル表示用でModifierを分けたことで、呼び出し側から用途を判別しやすくしています。

スクロールエッジ用:flatScrollEdgeEffect

スクロールエッジの設定もExtensionへまとめました。

//
//  View+FlatScrollEdgeEffectModifier.swift
//

import SwiftUI

#if os(iOS)
extension View {
    /// iOS 26のスクロールエッジエフェクトによる
    /// Liquid Glassの色変化を抑制する
    ///
    /// - Note: iOS 18以前では何もしない。
    @ViewBuilder
    public func flatScrollEdgeEffect() -> some View {
        if #available(iOS 26.0, *) {
            self.scrollEdgeEffectStyle(.hard, for: [.all])
        } else {
            self
        }
    }
}
#endif

呼び出し側では、ScrollViewListなどに付与します。

struct ItemListView: View {
    var body: some View {
        List {
            ForEach(0..<20) { index in
                Text("項目 \(index)")
            }
        }
        .flatScrollEdgeEffect()
    }
}

OSバージョンの判定をExtension内に閉じ込めているため、各画面で#availableを書く必要がありません。

Toolbar用:flatToolbar

NavigationBar上のToolbarについても、専用のExtensionを用意しました。

//
//  View+FlatToolbarModifier.swift
//

import SwiftUI

#if os(iOS)
extension View {
    /// NavigationBar上のToolbarを
    /// Liquid Glassのないフラットな見た目で表示する
    ///
    /// - Note: iOS 18以前では通常のtoolbarとして動作する。
    /// - Note: bottomBarなど、NavigationBar以外のToolbarには
    ///   通常のtoolbarを使用すること。
    @ViewBuilder
    public func flatToolbar<C: ToolbarContent>(
        @ToolbarContentBuilder content: () -> C
    ) -> some View {
        if #available(iOS 26.0, *) {
            self.toolbar {
                content()
                    .sharedBackgroundVisibility(.hidden)
            }
        } else {
            self.toolbar(content: content)
        }
    }
}
#endif

呼び出し側では、通常の.toolbarとほぼ同じ形で使用できます。

struct SettingsView: View {
    var body: some View {
        Text("設定画面")
            .navigationTitle("設定")
            .flatToolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        // 保存処理
                    } label: {
                        Text("保存")
                    }
                }
            }
    }
}

複数のToolbarItemを定義することもできます。

.flatToolbar {
    ToolbarItem(placement: .topBarLeading) {
        Button("キャンセル") {
            // キャンセル処理
        }
    }

    ToolbarItem(placement: .topBarTrailing) {
        Button("保存") {
            // 保存処理
        }
    }
}

flatToolbarは、NavigationBar上のToolbarを対象としています。

.bottomBarなど、ほかの配置を含むToolbarまで一律にラップすると、意図しない見た目になる可能性があります。

そのため、用途をNavigationBar上のボタンに限定しました。

Extension化によって得られたメリット

今回の対応をExtensionへまとめることで、呼び出し側がLiquid Glassの実装詳細を意識しなくてよくなりました。

画面側では、必要なModifierを付与するだけで対応できます。

struct DetailView: View {
    var body: some View {
        ScrollView {
            Text("コンテンツ")
        }
        .navigationTitle("詳細")
        .flatBackButton()
        .flatScrollEdgeEffect()
        .flatToolbar {
            ToolbarItem(placement: .topBarTrailing) {
                Button("編集") {
                    // 編集処理
                }
            }
        }
    }
}

画面側に#availablesharedBackgroundVisibilityが散らばらないため、コードを読む際にも画面本来の処理へ集中できます。

また、戻るボタンについては用途ごとにAPIを分けています。

// NavigationLinkなどのプッシュ遷移
.flatBackButton()

// sheetやfullScreenCoverなど
.flatCustomBackButton()

// dismiss以外の戻る処理が必要な場合
.flatCustomBackButton {
    // カスタム処理
}

メソッド名から利用場面を判断できるため、誤ったModifierを使用する可能性を減らせます。

今後のiOSアップデートによってAPIの挙動が変わった場合も、Extension側へ変更を集約できます。

SwiftLintとAIエージェントのRulesで抜け漏れを防ぐ

Extensionを用意しても、開発者が通常の.toolbar {}を使用するとLiquid Glassがそのまま適用されます。

また、AIエージェントを利用して実装する場合も、AIエージェントがプロジェクト固有の事情を知らなければ、当然のように標準の.toolbar {}を生成します。

そこで、次の2段構えで抜け漏れを防ぐことにしました。

  1. SwiftLintで直接的な.toolbar {}の使用をエラーにする
  2. AIエージェントのRulesに、使用すべきModifierと理由を記載する

SwiftLintで.toolbarの直接使用を禁止する

.swiftlint.ymlにカスタムルールを追加し、.toolbar {}または.toolbar(content:)を直接使用した場合はエラーにしました。

custom_rules:
  # iOS 26 Liquid Glass抑制のため、
  # .toolbarの直接使用を禁止し、.flatToolbar(content:)を強制する
  no_direct_toolbar:
    included:
      - ".*\\.swift"
    name: "No direct .toolbar"
    regex: "\\.toolbar\\s*(\\{|\\(content:)"
    message: "Use .flatToolbar(content:) instead of .toolbar {} to suppress Liquid Glass on iOS 26+."
    severity: error

これにより、通常の画面で次のようなコードを書くとSwiftLintのエラーになります。

.toolbar { // ⚠️ SwiftLintでエラーになる ⚠️
    ToolbarItem(placement: .topBarTrailing) {
        Button("保存") {
            // 保存処理
        }
    }
}

SwiftLintのエラーにすることで、ローカル開発時やCI上で機械的に検知できるようにしました。

AIエージェントのRulesにも使用方法を記載する

SwiftLintでは.toolbarの直接使用を検知できますが、次のような判断まではできません。

  • 通常遷移なのでflatBackButton()を使う
  • モーダル表示なのでflatCustomBackButton()を使う
  • WebViewの戻る処理なのでactionを指定する
  • ScrollViewを使用しているためflatScrollEdgeEffect()を付与する

そこで、AIエージェントが参照するRulesにも、Liquid Glass抑制の方針を記載しました。

#### Liquid Glass抑制モディファイアの使用必須

**理由**: iOS 26以降でNavigationBarにLiquid Glassエフェクトが
デフォルト適用されるため、本アプリのデザインに合わせて
コードレベルで抑制する。

> **注**: `.toolbar {}`の直接使用は、
> `.swiftlint.yml``no_direct_toolbar`ルールで自動検知される。

`SwiftUIHelper`が提供する以下のラッパーモディファイアを
必ず使用すること。

| 用途 | 使用するモディファイア | 備考 |
|---|---|---|
| ツールバー表示 | `.flatToolbar(content:)` | `.toolbar {}`の代わりに使用 |
| NavigationLink遷移先の戻るボタン | `.flatBackButton()` | システム戻るボタンがある通常遷移に使用 |
| モーダル内での戻るボタン(dismiss) | `.flatCustomBackButton()` | `sheet`/`fullScreenCover`内でシステム戻るボタンがない場合。タップで`dismiss()`を実行 |
| モーダル内での戻るボタン(カスタム動作) | `.flatCustomBackButton(action:)` | 戻るボタンに`dismiss`以外の独自動作が必要な場合 |
| ScrollView/Listを使う画面 | `.flatScrollEdgeEffect()` | スクロール時のナビゲーションバーの色変化を抑制 |

Rulesには、単に「このAPIを使うこと」だけではなく、なぜ必要なのか、どの場面でどのModifierを使うのかも記載しています。

これにより、AIエージェントが新しい画面を実装する際にも、プロジェクト固有のルールを踏まえたコードを生成しやすくなります。

対応しないと判断したこと

タブバーのLiquid GlassをOFFにする

タブバーのLiquid GlassをOFFにする場合、カスタムタブバーを自前で用意する必要があります。

標準のタブバーには、プッシュ遷移後にタブバーの表示・非表示を自動で切り替える挙動が組み込まれています。

これを独自に再現する場合、単に画面下部へボタンを並べるだけでは済みません。

表示・非表示の制御については、NavigationStack.toolbar(.hidden, for: .tabBar)を組み合わせる案も検討しました。

struct DetailView: View {
    var body: some View {
        Text("詳細画面")
            .toolbar(.hidden, for: .tabBar)
    }
}

ただし、このAPIはiOS 16以上限定です。

今回の案件ではサポート対象OSの制約に合わず、対応候補として検討したうえで見送りました。

また、既存の画面遷移はレガシーなNavigationViewで構築されていました。

.toolbar(.hidden, for: .tabBar)を使用するには、pushした階層を知るためにNavigationStackへの置き換えが前提となります。

この置き換えには、既存の画面遷移を洗い出して移行するコストが発生するため、単純にModifierを一つ追加して終わる対応ではありませんでした。

NavigationViewの構成を維持したまま、UIKitのhidesBottomBarWhenPushedに相当する挙動を実現する案も検討しました。

ただし、UIKitとの連携や独自の表示制御によって実装が複雑化する可能性が見込まれたため、対応を保留にしました。

さらに、プッシュ通知からの遷移なども考慮すると、タブバーの表示制御パターンが複雑化します。

実装・保守コストと見た目のメリットを天秤にかけ、タブバーはLiquid Glassを適用したまま進める判断としました。

メニューやsheetなどのLiquid GlassをOFFにする

メニューやsheetについては、メインとなる画面デザインへの影響が限定的だったため、今回は対応を見送りました。

すべてのLiquid Glassをなくすことを目標にすると、影響が小さい箇所にも工数を使うことになります。

今回は、ユーザーが長い時間操作する画面や、既存デザインとの差が目立つ箇所を優先しました。

まとめ

今回の対応をまとめると、次のようになります。

パーツ 対応 使用したAPI・方法 共通化したExtension 補足
NavigationBarのボタン OFF sharedBackgroundVisibility(.hidden) flatToolbar ナビゲーションバーの縦幅が長くなり、下部に余白が生まれる
NavigationBarの戻るボタン OFF カスタムボタンへ置き換え flatBackButton スワイプバックが使えなくなる
モーダル内の戻るボタン OFF カスタムボタンを表示 flatCustomBackButton 任意のカスタムアクションも指定可能
Toolbar OFF sharedBackgroundVisibility(.hidden) flatToolbar NavigationBar上のToolbarを対象とする
スクロールエッジ OFF scrollEdgeEffectStyle(.hard, for: [.all]) flatScrollEdgeEffect iOS 26以降にのみ適用する
タブバー 見送り - - NavigationStackへの移行や表示制御のコストが大きい
メニュー・sheet 見送り - - メインデザインへの影響が限定的

Info.plistUIDesignRequiresCompatibilityによる互換モードは、iOS 26で新しいデザインへ移行するために用意された一時的な対応です。

iOS 27以降も利用できる前提にはできないため、今回紹介したパーツ単位のAPIによる対応を行いました。

さらに、各対応をView Extensionへまとめるだけでなく、次の仕組みも追加しました。

  • OSバージョン判定をExtensionへ集約する
  • AIエージェントのRulesへ使用方法と設計意図を記載する
  • SwiftLintで.toolbarの直接使用を禁止する
  • CI上でもSwiftLintを実行し、ルール違反を検知する

工数の制約からLiquid GlassをOFFにする場合でも、すべてのパーツを一律に変更するのではなく、実装・保守コストとデザイン上の優先度を見ながら、対応範囲を絞ることが重要です。

どこまで対応し、どこから標準のデザインを受け入れるか。

今回は、パーツ単位でその妥協点を探りつつ、実装後の抜け漏れを仕組みで防ぐ形になりました。

この記事が、Liquid Glass対応で同じように現実と向き合っている方の参考になれば幸いです。

テコテックの採用活動について

テコテックでは新卒採用、中途採用共に積極的に募集をしています。 採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。 ご興味を持っていただけましたら是非ご覧ください。 tecotec.co.jp