フォーム画面を作ったとき、メールアドレスや本文を入力しようとするとキーボードが開いて下のフィールドが完全に見えなくなる——Rork でアプリを開発していると必ずといっていいほど直面する問題です。しかもこの現象、iOS と Android で挙動が微妙に違うので、片方で直したつもりがもう片方で再発する、という厄介な性格があります。
発生パターン別に原因を整理し、実際に動くコードとともに解決策を順番にご紹介します。
なぜキーボードが入力フィールドを隠すのか
React Native(Rork が内部で使うフレームワーク)では、ソフトウェアキーボードが展開されてもレイアウトは自動的に押し上げられません。Webブラウザとは異なり、キーボードの領域分だけ表示エリアが縮む仕組みがデフォルトで有効化されているわけではないのです。
iOS は behavior="padding" で比較的素直に対応できますが、Android は windowSoftInputMode の設定によって挙動が変わります。Expo managed workflow では app.json の設定でコントロールできます。
まず確認すること:app.json の softwareKeyboardLayoutMode
{
"expo": {
"android": {
"softwareKeyboardLayoutMode": "pan"
}
}
}Expo では softwareKeyboardLayoutMode を "pan" にするとキーボード表示時に画面全体が上にパンします。"resize" にするとコンテンツ領域が縮小されます。どちらが適しているかはアプリの構造によって変わりますが、ほとんどのフォーム画面では "pan" のほうが自然に見えます。
ただし "pan" にしても iOS では効果がなく、Android 専用の設定です。iOS には別のアプローチが必要です。
基本パターン:KeyboardAvoidingView を使う
import {
KeyboardAvoidingView,
Platform,
ScrollView,
TextInput,
StyleSheet,
View,
} from 'react-native';
export default function ContactForm() {
return (
// Platform.OS で iOS / Android を分岐するのがポイント
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
<TextInput
style={styles.input}
placeholder="名前"
returnKeyType="next"
/>
<TextInput
style={styles.input}
placeholder="メールアドレス"
keyboardType="email-address"
returnKeyType="next"
/>
<TextInput
style={styles.input}
placeholder="お問い合わせ内容"
multiline
numberOfLines={6}
textAlignVertical="top"
/>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
scrollContent: {
padding: 16,
paddingBottom: 40, // キーボードが閉じたあとの余白
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
marginBottom: 16,
fontSize: 16,
},
});重要なポイントが2つあります。
-
behaviorプロパティを必ずPlatform.OSで分岐すること。iOS は'padding'、Android は'height'が基本です。undefinedのままにすると iOS でまったく機能しません。 -
keyboardVerticalOffsetでナビゲーションバーの高さを調整すること。Expo Router のスタック画面では通常90前後が適切ですが、カスタムヘッダーを使っている場合は実測して合わせてください。
よくある間違い:ScrollView を忘れる
KeyboardAvoidingView だけ追加しても、中に ScrollView がなければ画面の内容が画面外に飛び出したとき追いかけられません。フォームの項目が画面に収まりきらない場合は必ず ScrollView を組み合わせてください。
また keyboardShouldPersistTaps="handled" を ScrollView に設定しないと、フィールド外をタップしてもキーボードが閉じなかったり、タップがボタンに届かないトラブルが起きます。これは忘れがちな設定です。
パターン2:useKeyboard フックで柔軟に制御する
KeyboardAvoidingView の動作が思い通りにならないケース(ボトムシートと組み合わせた時、アニメーション中にズレるなど)では、キーボードの高さを直接取得して自分でレイアウトを調整する方法が確実です。
import { useEffect, useState } from 'react';
import { Keyboard, KeyboardEvent, Platform } from 'react-native';
// キーボードの高さをリアルタイムに取得するカスタムフック
export function useKeyboardHeight() {
const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => {
const showEvent =
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent =
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
const showSub = Keyboard.addListener(
showEvent,
(e: KeyboardEvent) => {
setKeyboardHeight(e.endCoordinates.height);
}
);
const hideSub = Keyboard.addListener(hideEvent, () => {
setKeyboardHeight(0);
});
return () => {
showSub.remove();
hideSub.remove();
};
}, []);
return keyboardHeight;
}// 使い方
export default function ChatScreen() {
const keyboardHeight = useKeyboardHeight();
return (
<View style={{ flex: 1 }}>
<FlatList
data={messages}
// ...
/>
<View
style={{
paddingBottom: keyboardHeight > 0 ? keyboardHeight : 16,
borderTopWidth: 1,
borderTopColor: '#eee',
padding: 8,
}}
>
<TextInput placeholder="メッセージを入力..." />
</View>
</View>
);
}チャット画面のような「入力欄が画面下部に固定されている」UIではこの方法が最も安定します。keyboardHeight が変化するたびに再レンダリングが走りますが、チャット画面程度ではパフォーマンスへの影響は無視できます。
Rork Max(SwiftUI)の場合
Rork Max で SwiftUI ネイティブアプリを生成している場合は、React Native のアプローチは関係ありません。SwiftUI では .ignoresSafeArea(.keyboard) と ScrollView の組み合わせが基本です。
// SwiftUI でキーボード対応フォームを作る例
struct ContactFormView: View {
@State private var name = ""
@State private var email = ""
@State private var message = ""
var body: some View {
ScrollView {
VStack(spacing: 16) {
TextField("名前", text: $name)
.textFieldStyle(.roundedBorder)
TextField("メールアドレス", text: $email)
.textFieldStyle(.roundedBorder)
.keyboardType(.emailAddress)
TextEditor(text: $message)
.frame(height: 120)
.border(Color.gray.opacity(0.3))
}
.padding()
}
// これがないと TextEditor が画面下に来たとき隠れる
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}SwiftUI は iOS 15 以降、デフォルトでキーボード回避機能が内蔵されていますが、TextEditor(複数行テキスト入力)では明示的に .ignoresSafeArea(.keyboard) を指定しないと下部が隠れることがあります。
タブバーがあるとさらに複雑になる
タブベースのナビゲーション(expo-router の Tabs など)では、タブバーの高さ分だけ追加のオフセットが必要です。
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function FormWithTabs() {
const insets = useSafeAreaInsets();
const TAB_BAR_HEIGHT = 49; // Expo Router のデフォルト
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
// タブバー + セーフエリアの合計を引く
keyboardVerticalOffset={TAB_BAR_HEIGHT + insets.bottom}
>
{/* フォームの内容 */}
</KeyboardAvoidingView>
);
}keyboardVerticalOffset は「キーボードの基準位置をどこにするか」のオフセットです。タブバーがある画面では、タブバーの高さを正確に引いてあげないと計算がズレます。
全体を振り返って:選択肢を状況で使い分ける
最後に、どのアプローチを選ぶべきかの判断基準をまとめます。
- シンプルなフォーム(ログイン・問い合わせ):
KeyboardAvoidingView+ScrollViewで十分対応できます。 - チャット画面・下部固定の入力欄:
useKeyboardHeightフックで直接高さを管理するほうが安定します。 - Rork Max(SwiftUI):
.ignoresSafeArea(.keyboard)を使い、SwiftUIの仕組みに乗りましょう。 - タブバーありの画面:
keyboardVerticalOffsetにタブバーの高さを加算することを忘れないでください。
フォームのUI/UXをさらに磨きたい場合は、Rorkアプリのレイアウト・レスポンシブデザインのトラブルシューティングガイドも参考にしてください。入力バリデーションの実装についてはreact-hook-form と Zod を使ったフォームバリデーションで詳しく解説しています。