iOS 26のLiquid Glass:SwiftUIアプリをAppleの新しいデザイン言語に適応させる方法#
WWDC 2025でLiquid Glassが発表されたとき、多くのiOSエンジニアの最初の反応は「これはどう動くのか」ではなく「これから何画面作り直すことになるのか」だった。もっともな疑問だ。Liquid Glassは iOS 7のフラットデザインのような単なる見た目の刷新ではなく、独自の物理法則を持つマテリアルである。光を屈折させ、デバイスの動きやタッチに反応し、隣接する要素と溶け合い、状態間でモーフィング(形状変化)する。SwiftUI にはそのための専用 API 群が用意されており、古い UI にそれを機械的に貼り付けただけでは、システム標準の画面の隣で明らかに浮いて見えてしまう。
筆者は MeteoHealth を含む複数の自作アプリを Liquid Glass に対応させてきた。この記事では、実際に役立った知見をまとめている——2026年時点(iOS 26.1・26.2 での調整を含む)の最新 API、既存のカスタム UI を移行する際に繰り返し発生するミス、そしてユーザーが Reduce Transparency をオンにしたときにアプリで何が起きるか、という内容だ。
Liquid Glass とは何か——単なる blur の延長ではない#
iOS 26 以前は .ultraThinMaterial や .regularMaterial などの Material 系の静的な blur が主流で、透明度を調整できる程度だった。Liquid Glass はマテリアルの延長ではなく、独立したシステムレイヤーであり、次のような特徴を持つ。
- 背後のコンテンツを単にぼかすのではなく、屈折・反射させる
- タッチとポインタ操作に反応する——押すとわずかにバウンドしてハイライトする
- 近くにある glass 要素同士が視覚的に溶け合い、view 階層が変化する際には形状同士が モーフィング する
- 背後のコンテンツに応じて 自動的に適応する——明るい背景では縁がより高コントラストに、暗い背景ではより透明に見える
Apple が示す重要な設計原則は、Liquid Glass は 機能レイヤー(ナビゲーション、ツールバー、コントロール、一時的なオーバーレイ)のためのマテリアルであり、コンテンツレイヤー のためではない、ということだ。記事のカード、ギャラリーの写真、メッセージ一覧はコンテンツであり、技術的に可能だからといってそれをガラスにする必要はない。もう一つの原則は「ガラスの上にガラスを重ねない」こと。すでにシステムの navigation bar や sheet がガラス状になっている場所にさらに glassEffect を重ねると、期待した奥行きではなく濁った見た目になってしまう。
新しい API:glassEffect、Glass、そしてモディファイアの順序#
基本となるのは .glassEffect() モディファイアだ。パラメータなしで使うと、view は .regular バリアントの Capsule 形状で包まれる。
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect()形状は .rect(cornerRadius:)、.circle、.capsule などで明示的に指定でき、マテリアルの振る舞いは Glass 構造体を通じて設定する。.regular は基本のガラス、.tint(Color) はより重要な要素に色のアクセントを加え、.interactive() はタップ時のバネのような反応を有効にする。
struct WeatherSummaryCard: View {
let temperature: String
let condition: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(condition)
.font(.headline)
Text(temperature)
.font(.system(size: 34, weight: .semibold, design: .rounded))
}
.padding(20)
.frame(maxWidth: .infinity, alignment: .leading)
.modifier(AdaptiveGlassBackground(cornerRadius: 24))
}
}
/// Applies Liquid Glass on iOS 26+, falls back to Material on older systems.
/// Note the order: layout modifiers (padding, frame) come first,
/// the glass effect is applied last, on top of the finished layout.
struct AdaptiveGlassBackground: ViewModifier {
let cornerRadius: CGFloat
func body(content: Content) -> some View {
if #available(iOS 26.0, *) {
content.glassEffect(
.regular.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
} else {
content.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
)
}
}
}ここではモディファイアの順序が重要になる。glassEffect はレイアウトや見た目を決めるモディファイア(padding、frame、font)の あとに 適用しなければならない。マテリアルは view の最終的な境界を計測するため、レイアウトが確定した状態を見る必要があるからだ。.glassEffect() を .padding() より前に置くと、「ガラスが想定と違う境界で切れる」という典型的な不具合の原因になる。
GlassEffectContainer、モーフィング、glassEffectID:動的な状態管理#
近くに複数の glass 要素が並ぶ場合——例えばクイックアクションバーのような場合——は GlassEffectContainer で包む必要がある。このコンテナは共有のサンプリング領域を確立し、隣接する要素を単に重ねるのではなく、視覚的に溶け合わせることができる。
enum QuickAction: String, CaseIterable, Identifiable {
case refresh, share, favorite
var id: String { rawValue }
var symbolName: String {
switch self {
case .refresh: return "arrow.clockwise"
case .share: return "square.and.arrow.up"
case .favorite: return "heart"
}
}
}
struct QuickActionsBar: View {
@State private var isExpanded = false
@Namespace private var glassNamespace
var body: some View {
GlassEffectContainer(spacing: 24) {
HStack(spacing: 24) {
Button {
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
isExpanded.toggle()
}
} label: {
Image(systemName: isExpanded ? "xmark" : "plus")
.frame(width: 56, height: 56)
}
.buttonStyle(.glass)
.glassEffectID("toggle", in: glassNamespace)
if isExpanded {
ForEach(QuickAction.allCases) { action in
Button {
// handle action
} label: {
Image(systemName: action.symbolName)
.frame(width: 56, height: 56)
}
.buttonStyle(.glass)
.glassEffectID(action.id, in: glassNamespace)
.glassEffectUnion(id: "expanded", namespace: glassNamespace)
}
}
}
}
}
}ここで重要な点は 2 つある。まず、GlassEffectContainer の spacing は、隣接する要素がどのくらいの距離で視覚的に溶け合い始めるかを制御する——値が小さいほど、view 同士は近くにある必要がある。次に、glassEffectID と @Namespace の組み合わせは、ボタンの出現・消失を単純なクロスフェードではなく滑らかなモーフィングに変える仕組みだ。isExpanded の変更を withAnimation で包まなければ、この効果はそもそも発動しない。さらに glassEffectUnion は、共有の HStack の外——例えば動的な ForEach の中——で生成される要素を、単一の glass サーフェスとしてグループ化する。
iOS 26 未満へのフォールバック:ひとつのアプリに 2 つのデザイン言語#
もしアプリに iOS 26 未満のユーザーが存在するなら——実際のプロダクトのほとんどはあと 1〜2 年その状態が続くはずだ——フォールバックはオプションではなく、対応の必須要素になる。正しいパターンは、view ツリーを 2 系統並行して持つのではなく、上記の AdaptiveGlassBackground のように #available による分岐を単一のモディファイアに閉じ込めることだ。iOS 26 以降では .glassEffect(...)、それより古いシステムでは .background(.ultraThinMaterial, in:) を使い、形状(RoundedRectangle と .rect(cornerRadius:))を揃えることで、OS バージョンによってカードのジオメトリがずれないようにする。
このステップでよくあるミスは、.glassEffect() に isEnabled パラメータがあることを忘れてしまうことだ。view の内部で if #available を毎回書くより、これを使えば view の実装を一つに保ったまま、ツリーの上位で一度だけ計算したフラグでマテリアルの有無を切り替えられる。
カスタム UI を移行する際によくある間違い#
複数のアプリを移行した経験から、繰り返し起きるミスは共通している。
- ガラスの上にガラス。 すでにガラス状のツールバーを持つシステムの
sheetやNavigationStackの中で、カスタムカードにglassEffectを適用すると、不透明なグレーの塊になり、コントラストとテキストの可読性が落ちる。 - コンテンツレイヤーへの glass 適用。 記事のカバー画像、プロフィール写真、プレイヤーの背景はコンテンツであって機能要素ではない。それをガラスで「装飾」しようとすると、多くの場合むしろ読みにくくなり、プレミアム感は増さない。
- コンテナなしの独立した glass 要素の乱立。 各アイコンが
GlassEffectContainerなしにそれぞれglassEffect()を持つと、レンダリングコストは上がる一方で、本来期待していた視覚的な融合は起きない。 - モディファイアの順序ミス。
glassEffectをpaddingやframeより前に置くと、マテリアルが未確定のレイアウトを基準に境界を計測してしまう。 - Reduce Transparency の無視。
#available(iOS 26, *)だけをチェックして他は何もしないコードは、意図的にインターフェースの透明度を下げているユーザーの存在を無視している。
アクセシビリティ:Reduce Transparency、Tinted モード、そして省略できないこと#
iOS 26.1 から、Apple はユーザーに Liquid Glass を直接コントロールする手段を提供した。Accessibility → Display & Text Size の Reduce Transparency トグルはマテリアルの不透明度を高め、Settings → Display & Brightness → Liquid Glass には「Clear」と「Tinted」を切り替えるスイッチが追加された。後者はわずかに暗くすることでコントラストを高める。iOS 26.2 ではさらに、ロック画面の時計に透明度スライダーが追加された。
これは開発者にとって、accessibilityReduceTransparency が有効なときに glassEffect を手動で無効化する必要はない ということを意味する——システムがすでにマテリアルの不透明度を自動的に高めているからだ。自分で対応すべきなのは、.interactive() を付けるかどうかを判断する箇所でこの environment 値を考慮することだ。バネのようなアニメーションは、透明度とは別のアクセシビリティの論点だからである。
struct AccessibleGlassSurface<Content: View>: View {
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
let cornerRadius: CGFloat
@ViewBuilder var content: Content
var body: some View {
if #available(iOS 26.0, *) {
content
// Don't disable glassEffect manually when Reduce Transparency
// is on — iOS already increases frosting for the .regular
// variant. We only drop the extra .interactive() bounce,
// since motion is a separate accessibility concern.
.glassEffect(
reduceTransparency ? .regular : .regular.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
} else {
content.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
)
}
}
}これはデバイス上でシステム設定を何度も切り替えるだけでなく、プレビューでもテストしよう。#Preview の中で .environment(\.accessibilityReduceTransparency, true) を使えば、確認の手間をかなり省ける。
API マトリクス:対応時に何を変えるべきか#
| UI 要素 | iOS 26 以前 | iOS 26(Liquid Glass) |
|---|---|---|
| カード/パネル | .background(.ultraThinMaterial, in: RoundedRectangle(...)) | .glassEffect(.regular, in: .rect(cornerRadius:)) |
| アクションボタン | 手動の blur と shadow を持つカスタム ZStack | .buttonStyle(.glass) / .buttonStyle(.glassProminent) |
| 近接するアイコン群 | それぞれ個別のマテリアル | GlassEffectContainer + glassEffectUnion |
| 要素の出現・消失 | 隣接要素を考慮しない通常の .transition | モーフィング用の glassEffectID + @Namespace |
| Toolbar/tab bar accessory | カスタム背景、手動でのグループ分割 | 自動の glass サーフェス + ToolbarSpacer |
実際の適用例:MeteoHealth と対応チェックリスト#
MeteoHealth では、実際にガラス化したのは機能要素だけだった——ホーム画面のクイックアクションバー、体調グラフの上に浮かぶコントロール、現在の天気サマリーを表示する tab bar accessory。いずれもコンテンツの「上」に位置するものであり、コンテンツそのものではない。予報カードや指標の履歴は通常の背景のままにした。実際のユーザーテストで、密度の高い数値データの上にガラスを重ねると、インターフェースが心地よくなるどころか可読性が下がることがすぐに分かったからだ。
リリース前の最終チェックリスト。
- glass を適用しているのは機能レイヤー(ナビゲーション、ツールバー、一時的なコントロール)だけである
- 「ガラスの上にガラス」のケースがない(実際の sheet/NavigationStack で確認済み)
- 隣接する glass 要素は
GlassEffectContainerで包まれている -
glassEffectはレイアウト用モディファイアのあとに適用されている -
#available(iOS 26, *)によるMaterialへのフォールバックがあり、形状のジオメトリが一致している -
accessibilityReduceTransparencyでの挙動をプレビューと実機の両方で確認済み - Settings → Display & Brightness → Liquid Glass の Tinted モードを確認済み
- 状態間のモーフィングは
withAnimationで包まれている
Liquid Glass は、Apple が単なる化粧直しではなく、本物の物理法則と独自の合成ルールを持つマテリアルを投入した珍しい例だ。既存の Material の blur をそのまま glassEffect に置き換えるだけの対応は、2 つの glass 要素——あるいは glass 要素とシステムコンポーネント——が隣り合う最初の画面まではうまくいく。そこから先は API の構文の問題ではなく、アプリのどの層が機能でどの層がコンテンツなのかという問いになる。そして、その線引きこそが、対応がネイティブに見えるかどうかを何より左右する。
関連リンク:



