ExpoでApple Maps風のLiquid Glass(リキッドグラス)シートを作る方法(本当のやり方)
Expo UI、Expo Router、TrueSheet を使って、iOS 26 の新しい Liquid Glass ボトムシートを再現する方法を解説します。スムーズな detent(デタント)遷移と iOS 26 の挙動をできるだけ忠実に再現する手順と注意点をまとめています。
iOS 26 におけるシートの挙動
- 最低の detent(最小位置)ではシートは“浮いた”状態で、画面上にギャップがあり、角は完全に丸くなっています。
- 中間の detent にドラッグすると、まだ浮いた状態のままですが画面端に近づき、ギャップが狭まり角丸が変化します。
- 最高の detent にドラッグするとギャップが消え、浮いたカードのような挙動から通常のフルシートの挙動へ遷移します。
Apple Maps の最新挙動と同じインタラクションが目標です。
試したアプローチ
以下の 3 つのアプローチを試しました。Expo 環境で実現できる順に紹介します。
1) expo-swift-ui の BottomSheet
ネイティブ実装なので Liquid Glass をサポートしています。presentationDetents(または同等の設定)で detents を設定すれば、期待通りの挙動が得られます。ただし現時点では beta のため、本番に出すには注意が必要です。実験用途には最適です。
import { BottomSheet, Host } from "@expo/ui/swift-ui";
<Host style={{ position: "absolute", width }}>
<BottomSheet
isOpened={isOpened}
presentationDragIndicator="visible"
presentationDetents={[0.1, 0.5, 1]}
onIsOpenedChange={(e) => setIsOpened(e)}
>
<Text>Hello, world!</Text>
</BottomSheet>
</Host>
ここでは isOpened state でシートを制御しています。detents は 0 から 1(フル高さ)で指定します。presentationDragIndicator は上部のハンドル(ドラッグインジケーター)です。
2) Expo Router の formSheet プレゼンテーション
Evan Bacon が 𝕏(Twitter)で共有していた方法を試すと期待通りに動きました。基本的なシート挙動を手早く実現できます。
<Stack.Screen
name="liquidGlassSheet"
options={{
headerShown: false,
presentation: "formSheet",
gestureEnabled: false,
sheetGrabberVisible: true,
contentStyle: { backgroundColor: "transparent" },
sheetAllowedDetents: [0.1, 0.5, 1],
sheetInitialDetentIndex: 0,
sheetLargestUndimmedDetentIndex: 0,
}}
/>
ポイント:
- sheetGrabberVisible は上部のドラッグインジケーターです。
- Liquid Glass 表現を通すために contentStyle の backgroundColor を "transparent" にする必要があります。
- detents の範囲は 0 〜 1(前述)です。
- gestureEnabled: false にしておくと、ユーザーが下方向へドラッグして閉じるのを無効化できます。
- sheetInitialDetentIndex: 0 を設定すると初期位置が 0.1(シートが浮いた状態・角丸)になります。
この方法は「普通のシート」を手早く出すには完璧ですが、カスタムフッターやシートの移動に同期した細かなアニメーション制御が必要なケースでは柔軟性に欠けます。
3) TrueSheet(Jovanni Lo / @lodev09)
細かい制御やアニメーション同期が必要なら TrueSheet が非常に良い選択です。ネイティブ実装でシート背景のブラー(iOS 26 では Liquid Glass)をサポートし、detents、フッター、アニメーション値の取得などができます。
const sheet = useRef<TrueSheet>(null)
<TrueSheet ref={sheet} sizes={[75, "medium", "large"]} blurTint="default">
<View>
<Button title="Close Sheet" onPress={() => sheet.current?.dismiss()} />
</View>
</TrueSheet>
<Button title="Open Sheet" onPress={() => sheet.current?.present()} />
ポイント:
- シートは ref 経由で制御します(Gorhom の Sheet と同様)。
- sizes が detents を制御します。数値、パーセンテージ、または "auto", "medium", "large" のようなプリセット文字列を混在して使えます。
- Auto sizing が完全に安定しない場合は最低高さを明示的に指定(例: 75)してください。
- Liquid Glass 表現には blurTint="default" を使います。
- README がまだ最新でない可能性がありますが、ネイティブ API を呼んでいるため効果は問題なく動作します。
結論と推奨
- 迅速に試したい場合: Expo Router の formSheet は最も手軽で多くのケースに十分です。
- 細かい制御やアニメーション同期が必要な場合: TrueSheet が最も柔軟で本物に近い表現を提供します。
- expo-swift-ui はネイティブで直接 Liquid Glass を扱えますが、beta のため本番運用は慎重に。
detents の値は意図的に決めて、OS のレンダリングに任せる(vibe を活かす)ことをおすすめします。
React Native のアニメーションやトリックに興味がある方は、筆者が 𝕏(@iamarunabh)で投稿しているのでそちらもチェックしてください。