Rork で生成されたプロトタイプを iPhone 15 Pro の実機に流してみて、最初に違和感を覚えるのがこの瞬間ではないでしょうか。画面上部にあるはずのタイトルがノッチに重なり、下部のタブバーがホームインジケータに食い込んでいます。プレビュー画面ではきれいに見えていたのに、実機に出した途端にレイアウトが崩れる — 私自身、初めて Rork で作ったアプリを TestFlight に上げた時、まさにこの問題で30分ほど固まった記憶があります。
このレイアウト崩れは、Rork 固有のバグではありません。React Native / Expo で作るアプリ全般に共通する「Safe Area を考慮していない」という問題で、Rork が生成する初期コードに意図的に最小限の Safe Area 対応しか入っていないために顕在化しやすいのです。ここではなぜこの問題が起きるのかを整理したうえで、Rork で書き換える時にそのまま貼り付けて使える修正パターンをお伝えします。
なぜ Rork のプレビューでは気付けないのか
Rork の Web プレビューは、画面を四角い枠の中に表示する仕様になっています。つまり、プレビューには「ノッチ」「ダイナミックアイランド」「ホームインジケータ」「Android の3ボタンナビゲーション」といった実機特有の領域が存在しません。その結果、プレビューできれいに見えるレイアウトでも、実機に出した瞬間にコンテンツが端末のシステム UI と重なってしまいます。
具体的に重なるのは次の領域です。
- iPhone X 以降のノッチ・ダイナミックアイランド領域(上部約 44〜59pt)
- iPhone のホームインジケータ領域(下部約 34pt)
- Android のステータスバー(上部約 24dp)
- Android のナビゲーションバー(下部約 48dp、ジェスチャーナビゲーションは異なる)
これらの領域を避けてコンテンツを配置するための仕組みが、React Native では SafeAreaView と useSafeAreaInsets の2つです。Rork 生成コードでは片方しか入っていなかったり、ネスト構造の都合で効かなかったりするケースが多く見られます。
最小限で直す: SafeAreaView で囲むだけのパターン
一番シンプルな修正は、画面全体を SafeAreaView で囲むことです。ヘッダーもフッターもない一覧画面なら、これだけで直ります。
import { SafeAreaView } from "react-native-safe-area-context";
import { ScrollView, View, Text, StyleSheet } from "react-native";
export default function ArticleList() {
return (
<SafeAreaView style={styles.safe} edges={["top", "bottom"]}>
<ScrollView>
<View style={styles.header}>
<Text style={styles.title}>記事一覧</Text>
</View>
{/* 記事リスト */}
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: "#fff" },
header: { padding: 16 },
title: { fontSize: 22, fontWeight: "700" },
});ポイントは react-native-safe-area-context の SafeAreaView を使うことです。React Native 標準の SafeAreaView は iOS 専用のため、Android では効きません。Expo プロジェクトではデフォルトで react-native-safe-area-context が入っているので、そのまま import すれば動きます。
edges プロパティで、どの方向の safe area を適用するかを選べます。ボトムタブバーがある画面では edges={["top"]} を指定し、下部は自分で処理すると後述のタブバー対応がスムーズになります。
タブバー・ヘッダーがある画面: useSafeAreaInsets を使う
タブバーやカスタムヘッダーがある画面は、SafeAreaView だけでは不十分です。コンテンツ領域とバー領域で別々の余白計算が必要になるため、useSafeAreaInsets で数値を取得して手動で当てます。
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
export default function TabBarLayout({ children }: { children: React.ReactNode }) {
const insets = useSafeAreaInsets();
return (
<View style={styles.container}>
{/* カスタムヘッダー: ノッチ分の余白を足す */}
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
<Text style={styles.headerTitle}>ホーム</Text>
</View>
{/* コンテンツ領域 */}
<View style={styles.content}>{children}</View>
{/* カスタムタブバー: ホームインジケータ分の余白を足す */}
<View style={[styles.tabBar, { paddingBottom: insets.bottom + 8 }]}>
<TouchableOpacity style={styles.tab}>
<Text>Home</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.tab}>
<Text>Profile</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#fff" },
header: {
paddingHorizontal: 16,
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: "#eee",
},
headerTitle: { fontSize: 18, fontWeight: "600" },
content: { flex: 1 },
tabBar: {
flexDirection: "row",
paddingTop: 8,
paddingHorizontal: 16,
borderTopWidth: 1,
borderTopColor: "#eee",
},
tab: { flex: 1, alignItems: "center" },
});期待する動作は、iPhone 15 Pro ではヘッダー上部に約 59pt、タブバー下部に約 34pt の余白が自動で入り、古い iPhone SE では上部に約 20pt、下部に 0pt になります。Android でも同様に端末のステータスバー・ナビゲーションバーの高さが自動で反映されます。
よくあるハマりどころ
① SafeAreaProvider で囲んでいない
useSafeAreaInsets は SafeAreaProvider の子孫でないと全て0を返します。Rork 生成コードでは _layout.tsx や App.tsx にこのプロバイダが入っていないことがあります。その場合は次のように最上位を囲んでください。
// app/_layout.tsx (Expo Router) or App.tsx
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<SafeAreaProvider>
<Stack />
</SafeAreaProvider>
);
}② 背景色がノッチ部分で変わって見える
SafeAreaView の外側と内側で背景色が異なると、ノッチ周辺で色が切り替わる違和感が出ます。ヘッダーの色を画面全体に見せたい場合は、SafeAreaView ではなく View でルートを作り、中で paddingTop: insets.top を当てる形にすると、ノッチ背後まで色が伸びた一体感のあるデザインになります。
③ ScrollView の中で SafeAreaView を使っている
SafeAreaView は画面全体のラッパーとして使うもので、ScrollView の内側に入れると意図した動きになりません。ScrollView をラップする側に置きましょう。ScrollView のコンテンツが下まで伸びてほしい場合は contentContainerStyle={{ paddingBottom: insets.bottom }} を指定するのが定番のパターンです。
④ モーダル内で insets がゼロになる
iOS のモーダル表示(presentation: "modal")の中でも safe area は必要ですが、モーダル自体の形状(角丸・上部が少し下がる)でノッチは避けられるため、上部 inset は自動で0に近くなります。一方、下部のホームインジケータは残るため、insets.bottom を下部余白として使ってください。
実機での確認手順
Rork での修正は Web プレビューでは検証できないため、実機または Expo Go で確認します。Rork プロジェクトを GitHub にエクスポートし、Expo Go で開くのが最速です。
- Rork 画面右上の Export から GitHub へエクスポート
- ローカルで
npx expo startを実行 - iPhone 実機 + Expo Go で QR コードを読み込み
- iPhone 15 Pro と iPhone SE、Android 実機の3パターンで見た目を確認
ノッチがある機種と無い機種の両方で崩れないことが確認できれば完了です。Expo Go の Device のメニューから「Device Frame」を切り替えると、複数機種での表示をすぐに比較できるので便利です。
この話題は、iOS と Android で挙動が違う時の原因切り分けガイド や キーボード表示時に入力欄が隠れる問題の直し方 とも密接に関わります。レイアウトの崩れが複数箇所で起きているなら、レイアウトとレスポンシブデザインのトラブルシューティング も合わせて読むと、原因の切り分けがスムーズになります。
まずは今開発中のアプリの最上位 _layout.tsx を開いて、SafeAreaProvider が入っているかだけ確認してみてください。入っていなければ5分で追加でき、それだけで半分以上のレイアウト崩れが消えます。残りの崩れは useSafeAreaInsets で個別に数値を足していけば、実機で見ても違和感のないアプリに仕上がります。