◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-04-29中級

Rork アプリに Bottom Sheet を組み込む実践ガイド — @gorhom/bottom-sheet で iOS と Android の操作感を揃える

Rork が生成する Modal は便利ですが、複数のスナップポイントや慣性スクロールが必要になった瞬間に物足りなくなります。@gorhom/bottom-sheet を後から差し込む実装手順、キーボード対応、iOS/Android の差分対処までを実例で解説します。

Rork585Bottom Sheet@gorhom/bottom-sheetReact Native239UI 実装Reanimated10Gesture Handler

Rork で生成したアプリを少し触っていくと、ある瞬間に「標準の Modal ではどうしても物足りない」場面が出てきます。地図の下からスッと現れる詳細パネル、商品一覧の上に重ねるフィルター、入力フォームを途中までだけ表示するシート — どれも単純な全画面モーダルでは表現できません。

私自身、自分のアプリに「2段階で開くシート」を入れたかったのですが、Rork が出してくれた Modal ベースのコードでは慣性スクロールが効かず、Android で背景が真っ黒に光る問題に半日悩みました。結論としては @gorhom/bottom-sheet を後から差し込むのが一番素直で、Rork の出力をほとんど壊さずに導入できます。ここではその手順とつまずきやすいポイントをまとめておきます。

なぜ Rork が出す Modal だけでは足りなくなるのか

Rork は React Native 標準の Modal を使った実装をよく出してきます。確認ダイアログや軽い入力パネル程度なら十分機能しますが、次のような要件が重なると一気に苦しくなります。

  • シートの「半開き」「全開き」など複数のスナップポイントを切り替えたい
  • 中身に長いリストがあり、シート内で慣性スクロールさせたい
  • ドラッグの途中で戻したり、しきい値で自動的に開閉させたい
  • iOS 風の角丸・グレースケールの背景フェードを揃えたい

Modal でこれらを再現しようとすると、自前で PanResponder を書き、リスト内のスクロールと外側のドラッグを切り分け、Animated.Value を慎重に扱う必要があります。書けないことはないのですが、保守を考えると専用ライブラリに任せたほうが現実的です。

@gorhom/bottom-sheet を Rork プロジェクトに追加する

Rork は内部的に Expo + React Native のセットアップを使っています。@gorhom/bottom-sheet は Reanimated と Gesture Handler を前提にしているため、まずこの2つが入っているかを確認してから本体を追加します。

# Rork プロジェクトのルートで実行
npx expo install react-native-reanimated react-native-gesture-handler
npm install @gorhom/bottom-sheet

Reanimated は Babel プラグインの登録が必須です。babel.config.js にすでに設定が入っていることが多いですが、もし無ければ末尾に追記します。プラグインの順番が変わるとビルドが通らなくなることがあるので、必ず plugins 配列の最後に置いてください。

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      // ...他のプラグイン...
      'react-native-reanimated/plugin', // ← 必ず最後
    ],
  };
};

Gesture Handler はアプリのエントリポイントで GestureHandlerRootView がルートを覆っている必要があります。Rork が出した _layout.tsx の最外側に1行足すだけで済むことがほとんどです。

// app/_layout.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Slot } from 'expo-router';
 
export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Slot />
    </GestureHandlerRootView>
  );
}

ここまでで、Rork の生成コードはほとんど触っていません。後付けで Bottom Sheet を入れたい場合の最小限の準備はこれで完了です。

最小構成のボトムシートを動かす

まずは「半分まで開く / 全画面まで開く」の2スナップポイントだけを持つシートを作ります。useRef で参照を取り、ボタンから expand() / close() を呼べる形にしておくと、後から AI 生成された画面に組み込みやすくなります。

import React, { useCallback, useMemo, useRef } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet';
 
export default function PlaceDetailScreen() {
  const sheetRef = useRef<BottomSheet>(null);
 
  // 端末の高さに対するパーセンテージで指定できる
  const snapPoints = useMemo(() => ['25%', '60%', '90%'], []);
 
  const openSheet = useCallback(() => {
    sheetRef.current?.snapToIndex(1); // 60% の位置で開く
  }, []);
 
  return (
    <View style={styles.container}>
      <Button title="詳細を見る" onPress={openSheet} />
 
      <BottomSheet
        ref={sheetRef}
        index={-1} // 初期は閉じておく
        snapPoints={snapPoints}
        enablePanDownToClose
        backgroundStyle={styles.sheetBg}
        handleIndicatorStyle={styles.indicator}
      >
        <BottomSheetView style={styles.sheetBody}>
          <Text style={styles.title}>カフェ・ヒロカワ</Text>
          <Text>営業時間: 10:00 - 19:00</Text>
          <Text>東京都渋谷区...</Text>
        </BottomSheetView>
      </BottomSheet>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  sheetBg: { backgroundColor: '#ffffff' },
  indicator: { backgroundColor: '#cccccc', width: 48 },
  sheetBody: { padding: 20 },
  title: { fontSize: 20, fontWeight: '600', marginBottom: 8 },
});

期待する挙動は次の通りです。ボタンを押すとシートが下から60%の位置までスッと現れ、上にドラッグすれば90%、下にドラッグすれば25%もしくは閉じる、の3段階で止まります。enablePanDownToClose を入れておかないと下方向にドラッグして消すことができないので忘れないでください。

スナップポイントとジェスチャの設計で意識すること

スナップポイントは「ピクセル」ではなく「比率」で書くのが基本です。iPhone SE と iPad で見た目を揃えるためには、画面サイズに連動した比率指定のほうが扱いやすいからです。一方で、コンテンツの中身が決まっている場合は 'CONTENT_HEIGHT' を渡して中身の高さに合わせるという選択肢もあります(v4 以降)。

スナップポイントを増やしすぎると、ユーザーがどこで止めればいいか分からなくなります。私は「閉じる + 半開き + 全開き」の3つで止めることが多く、4つ以上はほぼ使いません。地図系アプリで Apple Maps のような挙動を再現したいときだけ、最下段に「ハンドルだけ見える」位置(10%程度)を追加します。

ドラッグ操作を細かく制御したい場合は onChange でインデックスを監視し、特定の位置から先に行ったらフォーカスを当てる、といった実装も可能です。ただし、ジェスチャの結果に副作用を被せると Reanimated のワーレットスレッドで実行されるため、UI スレッドへ戻す処理を runOnJS で囲む必要があります。この辺りの注意点は Rork × Reanimated と Gesture Handler で作る高度なアニメーション にまとめています。

入力フォームを乗せるときのキーボード対応

Bottom Sheet にテキスト入力を置くと、必ず一度はキーボードに隠される事故が起きます。@gorhom/bottom-sheet は専用の BottomSheetTextInput を提供しており、これを使うとフォーカス時に自動でシートを上にずらしてくれます。

import BottomSheet, {
  BottomSheetView,
  BottomSheetTextInput,
} from '@gorhom/bottom-sheet';
 
<BottomSheet
  ref={sheetRef}
  index={-1}
  snapPoints={['40%', '90%']}
  keyboardBehavior="interactive" // iOS で自然に追従
  keyboardBlurBehavior="restore" // 閉じる時に元の位置へ
  android_keyboardInputMode="adjustResize"
>
  <BottomSheetView style={{ padding: 16 }}>
    <BottomSheetTextInput
      placeholder="メモを入力..."
      style={{
        height: 44,
        paddingHorizontal: 12,
        backgroundColor: '#f4f4f4',
        borderRadius: 8,
      }}
    />
  </BottomSheetView>
</BottomSheet>

keyboardBehavior は iOS 限定のオプションで、'interactive' にしておくとキーボードと連動してシートが滑らかに動きます。Android 側は android_keyboardInputMode="adjustResize" を指定し、加えて app.json の softwareKeyboardLayoutMode を "resize" に変更してください。ここを "pan" のままにしているとシート全体が押し上げられて見た目が崩れます。

複数の入力欄があるフォームでは、フォーカスの切り替え時に若干の遅延が起きることがあります。これは BottomSheetScrollView を内側に使うことで解消できることが多く、リスト上にフォームを配置するパターンでも自然なスクロールになります。

iOS と Android で挙動が分かれるポイント

実機で触ると、思ったよりも OS 間の差が目立ちます。私が踏みやすかった3点だけ書いておきます。

  • 背景の暗転: iOS は半透明の黒が綺麗にフェードしますが、Android は端末によって BackdropComponent を渡さないと真っ黒になることがあります。@gorhom/bottom-sheet には BottomSheetBackdrop が付属しているので、明示的に渡すのが安全です。
  • ハンドルのドラッグ可能領域: Android は標準だとハンドル部分の判定が小さく、指が滑ります。handleHeight を 28〜32 程度まで広げると操作感が自然になります。
  • 戻るボタンとの連動: Android では物理戻るボタンでシートだけを閉じたいことがあります。useFocusEffect で BackHandler を登録し、sheetRef.current?.close() を呼んで true を返すと意図通りに動きます。

これらは「Rork が生成したデフォルトコード」では考慮されないことがほとんどなので、Bottom Sheet を入れた直後にレビュー実機(できれば中価格帯の Android)で必ず確認することをおすすめします。実機特有のクラッシュや表示崩れを早期に拾いたい場合は、Rork で作ったアプリが実機でクラッシュする時の切り分け方 も併せて読んでみてください。

なお、シート内のローディング表示は標準のスピナーよりもスケルトンを使うほうが体感が良くなります。実装パターンは Rork でスケルトンスクリーンを実装してロード待ちを快適にする にまとめてあるので、組み合わせて使うと完成度が上がります。

まずは小さく組み込んでみる

Rork が出してくれる Modal で十分な場面では、無理に Bottom Sheet を導入する必要はありません。ただ「半開き」や「上から重ねるリスト」を作りたいと思った瞬間に、自前実装と既製ライブラリで悩む時間が一気に増えます。そのときの選択肢として @gorhom/bottom-sheet をひとつ持っておくと、Rork の生成コードと無理なく共存できる、というのが3アプリほど触ってみた感想です。

最初の一歩としては、すでにある画面のうち1つだけシート化してみるのが取り組みやすいと思います。設定ボタンを押すと下から出てくるカテゴリ選択、地図の下に出てくる場所詳細など、効果が分かりやすい画面から差し替えてみてください。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ 開発ツール2026-09-04
EAS の secret は「アプリに入れない」設定ではありません — 接頭辞と可視性を別々に決める
EXPO_PUBLIC_ の接頭辞は成果物に入るかを決め、EAS の可視性は誰が読めるかを決めます。二つを重ねたときに OTA 更新でだけ値が消える筋道と、出荷前に自分の手で確かめる手順をまとめました。
⬡ 開発ツール2026-08-22
一括置換は全ファイル成功しました。壊れたのは、消さなかった行のほうです
生成コードに一括置換をかけると、一致した行ではなく隣の行が壊れます。運用中のプロジェクトで実際に起きた破損と、置換の不変条件を検査する20行のガードスクリプトをまとめました。
⬡ 開発ツール2026-08-14
Expo SDK 57 に上げる前に、prebuild で消えるネイティブ変更を洗い出す
Expo SDK 57 では expo prebuild が既定で ios と android を消してから再生成します。手編集を上げる前に洗い出す方法、config plugin への移し替え、57.0.9 で解消したメモリ回帰までを実務の順序でまとめました。