This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

画面を構築する

ページを編集

このチュートリアルでは、React Native の Pressable、Expo Image、Expo UI などのコンポーネントを使って画面を構築する方法を学びます。


この章では、StickerSmash アプリの最初の画面を作成します。

上記の画面には、画像と 2 つのボタンが表示されています。ユーザーはどちらかのボタンを使って画像を選択できます。1 つ目のボタンではデバイスから画像を選択でき、2 つ目のボタンではアプリが用意したデフォルト画像をそのまま使用できます。

ユーザーが画像を選択すると、その画像にステッカーを追加できるようになります。それでは、この画面を作っていきましょう。

視聴:ユニバーサル Expo アプリで画面を構築する
視聴:ユニバーサル Expo アプリで画面を構築する

Pressable、Expo Image などのコアコンポーネントを使用して、StickerSmash アプリの最初の画面を構築し、画像選択のレイアウトを作成します。


1

画面を分解する

コードを書いて画面を構築する前に、いくつかの主要な要素に分解してみましょう。

主要な要素は 2 つです。

  • 画面の上半分にある大きな画像
  • 画面の下半分にある 2 つのボタン

1 つ目のボタンは複数のコンポーネントで構成されています。背景は黄色です。その内側に、行レイアウトでアイコンとテキストコンポーネントが配置されています。

UI を小さな部品に分解できたので、コーディングを始める準備が整いました。

2

画像を表示する

アプリ内で画像を表示するために expo-image ライブラリを使用します。このライブラリは、画像を読み込んで描画するためのクロスプラットフォームな <Image> コンポーネントを提供します。使用しているデフォルトのプロジェクトテンプレートにすでに含まれています。

Image コンポーネントは画像のソースを値として受け取ります。ソースには 静的アセット または URL を指定できます。たとえば、assets/images ディレクトリから読み込むソースは静的アセットです。また、uri プロパティを使って ネットワーク から取得することもできます。

src/app/(tabs)/index.tsx ファイルで Image コンポーネントを使うには:

  1. expo-image ライブラリから Image をインポートします。
  2. assets/images/background-image.png を Image コンポーネントの source プロパティとして使用するために、PlaceholderImage 変数を作成します。
  3. 外側の View を react-native-safe-area-context の SafeAreaView に置き換えます。ネイティブタブはヘッダーを追加しないため、SafeAreaView を使うと Android と iOS で画面のコンテンツがステータスバーの下に配置されます。デフォルトテンプレートにはこのライブラリが含まれています。
  4. Platform.select() を使って、画像コンテナに paddingTop を追加します。web ではタブリストがページの上部に配置されるため、画像にはより多くのスペースが必要です。
src/app/(tabs)/index.tsx
import { Platform, View, StyleSheet } from 'react-native'; import { Image } from 'expo-image'; import { SafeAreaView } from 'react-native-safe-area-context'; const PlaceholderImage = require('@/assets/images/background-image.png'); export default function Index() { return ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <Image source={PlaceholderImage} style={styles.image} /> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, image: { width: 320, height: 440, borderRadius: 18, }, });

3

コンポーネントをファイルに分割する

この画面にコンポーネントを追加していく中で、コードを複数のファイルに分割しましょう。チュートリアル全体を通して、カスタムコンポーネントの作成には components ディレクトリを使用します。

  1. src 内に components ディレクトリを作成し、その中に image-viewer.tsx ファイルを作成します。
  2. 画像を表示するコードと image スタイルをこのファイルに移動します。
src/components/image-viewer.tsx
import { StyleSheet } from 'react-native'; import { Image, type ImageSource } from 'expo-image'; type Props = { imgSource: ImageSource; }; export default function ImageViewer({ imgSource }: Props) { return <Image source={imgSource} style={styles.image} />; } const styles = StyleSheet.create({ image: { width: 320, height: 440, borderRadius: 18, }, });

ImageViewer をインポートして、src/app/(tabs)/index.tsx で使用します。

src/app/(tabs)/index.tsx
import { Platform, StyleSheet, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import ImageViewer from '@/components/image-viewer'; const PlaceholderImage = require('@/assets/images/background-image.png'); export default function Index() { return ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} /> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, });
import 文の @ とは何ですか?

@ 記号は、相対パスの代わりにカスタムコンポーネントや他のモジュールをインポートするためのカスタム パスエイリアス です。Expo CLI が tsconfig.json で自動的に設定します。

4

Pressable を使ってボタンを作成する

React Native にはタッチイベントを扱うためのコンポーネントがいくつかありますが、柔軟性が高い <Pressable> が推奨されます。シングルタップ、長押し、押下時と解放時で別々のイベントの発火など、さまざまな操作を検出できます。

デザイン上、作成すべきボタンは 2 つあります。それぞれスタイルとラベルが異なります。まずは、これらのボタンの再利用可能なコンポーネントを作成しましょう。src/components ディレクトリ内に button.tsx ファイルを作成し、次のコードを記述します。

src/components/button.tsx
import { StyleSheet, View, Pressable, Text } from 'react-native'; type Props = { label: string; onPress?: () => void; }; export default function Button({ label, onPress }: Props) { return ( <View style={styles.buttonContainer}> <Pressable style={styles.button} onPress={onPress}> <Text style={styles.buttonLabel}>{label}</Text> </Pressable> </View> ); } const styles = StyleSheet.create({ buttonContainer: { width: 320, height: 68, alignItems: 'center', justifyContent: 'center', }, button: { height: 48, paddingHorizontal: 20, borderRadius: 24, alignItems: 'center', justifyContent: 'center', flexDirection: 'row', gap: 8, }, buttonLabel: { color: '#fff', fontSize: 16, }, });

onPress プロパティを使うと、ユーザーがボタンを押したときの動作を画面側で決められます。このコンポーネントを src/app/(tabs)/index.tsx ファイルにインポートしましょう。各ボタンに、alert() を呼び出す onPress 関数を渡します。次に、ボタンを囲む <View> のスタイルを追加します。

src/app/(tabs)/index.tsx
import { Platform, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; const PlaceholderImage = require('@/assets/images/background-image.png'); export default function Index() { return ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} /> </View> <View style={styles.footerContainer}> <Button label="Choose a photo" onPress={() => alert('You pressed a button.')} /> <Button label="Use this photo" onPress={() => alert('You pressed a button.')} /> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, });

画面上のいずれかのボタンをタップすると、アプリはアラートを表示します。Android、iOS、web でアプリを確認してみましょう。

「Use this photo」のラベルが付いた 2 つ目のボタンは、デザイン通りのボタンに近い見た目になっています。一方、1 つ目のボタンはデザインに合わせるために追加のスタイル設定が必要です。

5

再利用可能なボタンコンポーネントを強化する

「Choose a photo」ボタンには「Use this photo」ボタンとは異なるスタイルが必要です。primary テーマを適用するために、theme プロパティを追加しましょう。このボタンにはラベルの前にアイコンも表示します。アイコンの表示には expo-symbols の SymbolView コンポーネントを使用します。Android と web では、SymbolView は Material Symbols のアイコンを表示します。iOS では、SF Symbols のアイコンを表示します。デフォルトテンプレートにはすでに expo-symbols が含まれています。

src/components/button.tsx を修正して、次のコードスニペットを追加します。

src/components/button.tsx
import { StyleSheet, View, Pressable, Text } from 'react-native'; import { SymbolView } from 'expo-symbols'; type Props = { label: string; theme?: 'primary'; onPress?: () => void; }; export default function Button({ label, theme, onPress }: Props) { if (theme === 'primary') { return ( <View style={styles.buttonContainer}> <Pressable style={[styles.button, styles.primaryButton]} onPress={onPress}> <SymbolView name={{ ios: 'photo', android: 'image', web: 'image' }} size={18} tintColor="#25292e" /> <Text style={[styles.buttonLabel, { color: '#25292e' }]}>{label}</Text> </Pressable> </View> ); } return ( <View style={styles.buttonContainer}> <Pressable style={styles.button} onPress={onPress}> <Text style={styles.buttonLabel}>{label}</Text> </Pressable> </View> ); } const styles = StyleSheet.create({ buttonContainer: { width: 320, height: 68, alignItems: 'center', justifyContent: 'center', }, button: { height: 48, paddingHorizontal: 20, borderRadius: 24, alignItems: 'center', justifyContent: 'center', flexDirection: 'row', gap: 8, }, primaryButton: { backgroundColor: '#ffd33d', }, buttonLabel: { color: '#fff', fontSize: 16, }, });

上記のコードが何をしているのか確認しましょう。

  • primary テーマのボタンは、<Pressable> の style プロパティに配列を渡します。React Native はスタイルを順番に適用するため、styles.primaryButton の backgroundColor は primary ボタンにのみ適用されます。
  • primary ボタンのラベルは インラインスタイル を使用します。インラインスタイルとは、style プロパティに直接渡すオブジェクトです。これはラベルの色を #25292e に変更し、styles.buttonLabel の color を上書きします。
  • SymbolView の name プロパティは、プラットフォームごとのアイコン名を受け取ります。Android と web では image、iOS では photo です。

次に、src/app/(tabs)/index.tsx ファイルを修正して、1 つ目のボタンに theme="primary" プロパティを設定します。

src/app/(tabs)/index.tsx
import { Platform, View, StyleSheet } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Button from '@/components/button'; import ImageViewer from '@/components/image-viewer'; const PlaceholderImage = require('@/assets/images/background-image.png'); export default function Index() { return ( <SafeAreaView style={styles.container}> <View style={styles.imageContainer}> <ImageViewer imgSource={PlaceholderImage} /> </View> <View style={styles.footerContainer}> <Button theme="primary" label="Choose a photo" onPress={() => alert('You pressed a button.')} /> <Button label="Use this photo" onPress={() => alert('You pressed a button.')} /> </View> </SafeAreaView> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#25292e', alignItems: 'center', }, imageContainer: { flex: 1, paddingTop: Platform.select({ web: 96, default: 20 }), }, footerContainer: { flex: 1 / 3, alignItems: 'center', }, });

Android、iOS、web でアプリを確認してみましょう。

6

Expo UI でネイティブボタンを使用する

現在のボタンは React Native のコンポーネントを使用しているため、どのプラットフォームでも同じ見た目になります。Expo UI はネイティブコンポーネントを提供します。Android では Jetpack Compose のビュー、iOS では SwiftUI のビューです。デフォルトテンプレートにはすでに Expo UI(@expo/ui)が含まれています。Expo UI は Expo Go で動作します。

Android と iOS のボタンをネイティブボタンに置き換え、web では React Native のボタンをそのまま使いましょう。そのために、プラットフォーム固有の拡張子を使用します。button.tsx と同じ場所に button.android.tsx と button.ios.tsx があると、@/components/button をインポートしたときに、Metro は現在のプラットフォーム用のファイルを使用します。web では引き続き button.tsx が使われます。詳細については プラットフォーム固有の拡張子 を参照してください。

Android のボタンには、@expo/material-symbols のアイコンを表示します。これは Jetpack Compose 向けの Material Symbols アイコンのセットです。インストールするには、ターミナルで Ctrl + C を押して開発サーバーを停止してから、次のコマンドを実行します。

Terminal
- npx expo install @expo/material-symbols
- yarn expo install @expo/material-symbols
- pnpm expo install @expo/material-symbols
- bun expo install @expo/material-symbols

2 つの新しいファイルを作成するまで、開発サーバーは停止したままにしておきます。サーバーの実行中にファイルを追加すると、Metro が button.tsx を使い続けることがあります。その場合は、サーバーを再起動してください。

src/components ディレクトリ内に button.android.tsx ファイルを作成します。

src/components/button.android.tsx
import { StyleSheet, View } from 'react-native'; import { Host, Button as ComposeButton, TextButton, Icon, Row, Text } from '@expo/ui/jetpack-compose'; type Props = { label: string; theme?: 'primary'; onPress?: () => void; }; export default function Button({ label, theme, onPress }: Props) { if (theme === 'primary') { return ( <View style={styles.buttonContainer}> <Host matchContents> <ComposeButton onClick={onPress} colors={{ containerColor: '#ffd33d', contentColor: '#25292e' }}> <Row verticalAlignment="center" horizontalArrangement={{ spacedBy: 8 }}> <Icon source={require('@expo/material-symbols/image.xml')} size={18} tint="#25292e" /> <Text color="#25292e">{label}</Text> </Row> </ComposeButton> </Host> </View> ); } return ( <View style={styles.buttonContainer}> <Host matchContents> <TextButton onClick={onPress} colors={{ contentColor: '#fff' }}> <Text color="#fff">{label}</Text> </TextButton> </Host> </View> ); } const styles = StyleSheet.create({ buttonContainer: { width: 320, height: 68, alignItems: 'center', justifyContent: 'center', }, });
Host とは何ですか?

Host は React Native と Expo UI をつなぐブリッジです。Android の Jetpack Compose コンポーネントと iOS の SwiftUI コンポーネントは、Host の内側でのみレンダリングされます。matchContents プロパティは、Host のサイズをコンテンツに合わせます。ここでのコンテンツはボタンです。詳細については、Jetpack Compose と SwiftUI の Host を参照してください。

次に、src/components ディレクトリ内に button.ios.tsx ファイルを作成します。

src/components/button.ios.tsx
import { StyleSheet, View } from 'react-native'; import { Host, Button as SwiftUIButton } from '@expo/ui/swift-ui'; import { buttonStyle, controlSize, foregroundStyle, tint } from '@expo/ui/swift-ui/modifiers'; type Props = { label: string; theme?: 'primary'; onPress?: () => void; }; export default function Button({ label, theme, onPress }: Props) { if (theme === 'primary') { return ( <View style={styles.buttonContainer}> <Host matchContents> <SwiftUIButton label={label} systemImage="photo" onPress={onPress} modifiers={[ buttonStyle('borderedProminent'), controlSize('large'), tint('#ffd33d'), foregroundStyle('#25292e'), ]} /> </Host> </View> ); } return ( <View style={styles.buttonContainer}> <Host matchContents> <SwiftUIButton label={label} onPress={onPress} modifiers={[buttonStyle('borderless'), controlSize('large'), foregroundStyle('#fff')]} /> </Host> </View> ); } const styles = StyleSheet.create({ buttonContainer: { width: 320, height: 68, alignItems: 'center', justifyContent: 'center', }, });
モディファイアとは何ですか?

モディファイアは、スタイル、サイズ、色など、Expo UI コンポーネントの見た目と動作をカスタマイズします。Jetpack Compose では @expo/ui/jetpack-compose/modifiers から、SwiftUI では @expo/ui/swift-ui/modifiers からインポートします。次に、button.ios.tsx のように、コンポーネントの modifiers プロパティに配列として渡します。詳細については、Jetpack Compose と SwiftUI のモディファイアを参照してください。

どちらのファイルも button.tsx と同じプロパティを持つため、src/app/(tabs)/index.tsx を変更する必要はありません。src/components ディレクトリには、3 つのバージョンのボタンが含まれるようになりました。

src
 components
  button.android.tsxAndroid で使用
  button.ios.tsxiOS で使用
  button.tsxweb で使用
  image-viewer.tsx
なぜ button.tsx を残すのですか?

button.web.tsx ファイルがないため、web では button.tsx が使われます。また、TypeScript は @/components/button の型を button.tsx から読み取ります。そのため、3 つのファイルはすべて同じプロパティを受け付ける必要があります。

npx expo start を実行して、開発サーバーを再び起動します。Android、iOS、web でアプリを確認してみましょう。

まとめ

第 3 章:画面を構築する

アプリの最初の画面の構築に向けて、初期デザインの実装が完了しました。

次の章では、デバイスのメディアライブラリから画像を選択する機能を追加します。

次へ:画像ピッカーを使用する