Open22

ReactNativeについて学んだことをメモしていく

りゅうたりゅうた

モーダルコンポーネントでモーダルにしたいところを囲む

<Modal visible={} animationType="slide">
...
</Modal>

visibleは開閉状態を真偽値で設定する必要があるので、useStateで初期値をfalseにしてonPressでfalseにするようにすれば実装可能
animationTypeは他にもnone, fadeがある。

りゅうたりゅうた

影の当てかた

Android

elevationプロパティで影の深さを数値で指定

{
  elevation: 4
}

ios

iosでは複数のプロパティを追加して影を実装する
• shadowColor: 影の色を指定。
• shadowOffset: 影のオフセットを指定。これはオブジェクトで、widthとheightを持つ。
• shadowOpacity: 影の不透明度を指定。値は0から1の範囲で指定。
• shadowRadius: 影の半径を指定。

{
  shadowColor: 'black',
  shadowOffset: { width: 0, height: 4 },
  shadowOpacity: 0.25,
  shadowRadius: 6
}

iosとAndroid両方に対応するためにPlatform.selectででプラットフォームごとに記述

import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    ...Platform.select({
      ios: {
        shadowColor: 'black',
        shadowOffset: { width: 0, height: 4 },
        shadowOpacity: 0.25,
        shadowRadius: 6,
      },
      android: {
        elevation: 8,
      },
    }),
  },
});
りゅうたりゅうた

グラデーションの実装

  1. expoからグラデーションをインポート
    npx expo install expo-linear-gradient
  2. LinerGradientコンポーネントで囲む
<ImageBackground
 source={require("../assets/images/background.png")}
 style={styles.rootScreen}
 imageStyle={styles.backgroundImage}
>
 ...
</ImageBackground>
りゅうたりゅうた

アイコンの実装

アイコンはexpoが既に持っているので新たにパッケージをインストールする必要はない。
こちらから使いたいアイコンを選んでimportとcomponentをcopyして貼り付ける

りゅうたりゅうた

フォントの実装

パッケージのインストール

npx expo install expo-font

ファイルのインポート

import { useFonts } from "expo-font";

フォントの読み込み

const [fontsLoaded] = useFonts({
  'open-sans': require('./assets/fonts/OpenSans-Regular.ttf'),
  'open-sans-bold': require('./assets/fonts/OpenSans-Bold.ttf')
});

ローディングのパッケージをインポート

AppLoadingパッケージのインストール:

フォントがロードされるまでのローディング状態の設定

  if(!fontsLoaded) {
    return <AppLoading />
  }

これによりフォントが読み込まれていない場合、AppLoading コンポーネントを使用してローディング状態を表示します。

あとはuesFontsで定義したキーの部分を使ってスタイルでfontFamilyの値に指定すればフォントが適用されます。

 fontFamily: 'open-sans-bold',
りゅうたりゅうた

プラットフォームごとにスタイルを当てる方法

Platform.OSを使って条件分岐する方法

import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    borderWidth: Platform.OS === 'android' ? 2 : 0,
  },
});

Platform.selectを使ってスタイルを定義する方法

import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  container: {
    borderWidth: Platform.select({
      ios: 0,
      android: 2,
    }),
  },
});

ファイル名にプラットフォーム名を付けて分ける方法

Title.jsが存在する場合
 - Title.ios.js : ios専用ファイル
 - Title.android.js : android専用ファイル

りゅうたりゅうた

デバイスの横向き対応

app.jsonの変更

 "orientation": "portrait",

から

 "orientation": "default",

に変更

useWindowDimesionsで動的に対応

importします。

import { useWindowDimensions } from "react-native";

コンポーネントのトップレベルで使用します。

const { width, height } = useWindowDimensions();

ここで取得したwidthはheightはデバイスの横幅と縦幅を動的に取得します。
useWindowDimensionsに似たDimensionsも存在するが、Dimensionsは縦画面でのデバイスの横幅・縦幅を取得するがレンダリングした際にその値が固定されてしまうので途中で横向きにしてもスタイルが変わらないからレイアウトが崩れる可能性大

りゅうたりゅうた

react-navigationの導入

まずはimportします。

npm install @react-navigation/native
npx expo install react-native-screens react-native-safe-area-context
npm install @react-navigation/native-stack

App.jsに記述

import { StatusBar } from "expo-status-bar";
import { NavigationContainer } from "@react-navigation/native";
import CategoryScreen from "./screens/CategoryScreen";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <>
      <StatusBar style="dark" />
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen name="MealsCategories" component={CategoryScreen} />
        </Stack.Navigator>
      </NavigationContainer>
    </>
  );
}
  • NavigationContainer : アプリケーション全体をナビゲーションコンテキストとして定義
  • Stack.Navigator : 画面の遷移が積み重なって管理していくスタック型のナビゲーションを定義
  • Stack.Screen : 実際にナビゲーションさせたいコンポーネントを指定し、それに名前をつけてどのコンポーネントに遷移するのかを指定
りゅうたりゅうた

headerの設定

 <Stack.Navigator
          screenOptions={{
            headerStyle: {
              backgroundColor: "#351401",
            },
            headerTintColor: "white",
            contentStyle: { backgroundColor: "#3f3f25" },
          }}
        >
          <Stack.Screen
            name="MealsCategories"
            component={CategoryScreen}
            options={{
              title: "All Categories",
            }}
          />
          <Stack.Screen name="MealsOverview" component={MealsOverviewScreen} />

Stack.ScreenのoptionでtitleやheaderStyleでスタイルを当てることができるが、アプリケーション全体に適用するにはStack.NavigatorにたいしてscreenOptionsで全体の設定をすることができる。

動的な設定方法

import { useLayoutEffect } from "react";
  useLayoutEffect(() => {
    const categoryTitle = CATEGORIES.find(
      (category) => category.id === catId
    ).title;
    navigation.setOptions({
      title: categoryTitle,
    });
  }, [catId, navigation]);

コンポーネントに遷移したときにtitleが表示されるまでラグがある場合はuseLayoutEffectを使用する。

Stack.Screenのoptionsでも設定できる。
      options={({ route, navigation }) => {
              const catId = route.params.categoryId;
              return {
                title: catId,
              };
            }}
りゅうたりゅうた

スタイルをコンポーネントにpropsで渡すこともできる

<MealDetails
        duration={selectedMeal.duration}
        complexity={selectedMeal.complexity}
        affordability={selectedMeal.affordability}
        textStyle={styles.detailText}
      />
const styles = StyleSheet.create({
  detailText: {
    color: 'white'
  }
});
function MealDetails({duration, affordability, complexity, style, textStyle}) {
  return (
    <View style={[styles.details, style]}>
      <Text style={[styles.detailItem, textStyle]}>{duration}m</Text>
      <Text style={[styles.detailItem, textStyle]}>{complexity.toUpperCase()}</Text>
      <Text style={[styles.detailItem, textStyle]}>{affordability.toUpperCase()}</Text>
    </View>
  );
}

propsで受け取って配列にして記述することで上書きできる。

りゅうたりゅうた

headerにボタンを追加する

headerの右にボタンを追加する実装はoptionsの中でheaderRightもしくはheaderLeftで定義可能。

<Stack.Screen name="MealDetail" component={MealDetailScreen} options={{
  headerRight: () => {
  return <Button title="ボタン" />
}
}}/>

コンポーネントに直接的に関係ことであればAppで記述しているStack.screenの中で記述しても問題ないが、コンポーネントの中と関係してくる実装をする場合はpropsでnavigationを受け取り、navigation.setOptionsでStack.screenで記述できるoptionを使うことができるようになる。

function MealDetailScreen({  navigation }) {

  function headerButtonPressHandeler() {
    console.log('Button pressed')
  }

  useLayoutEffect(() => {
    navigation.setOptions({
      headerRight: () => {
        return <Button title='Tap me!' onPress={headerButtonPressHandeler} />
      }
    })
  }, [navigation, headerButtonPressHandeler])

  return (
   ...省略します
  );
}
export default MealDetailScreen;
りゅうたりゅうた

styleの中にコールバック関数

style属性のコールバック関数の引数でタップしているかどうかを真偽値で受け取ることができるので、状況に応じてスタイルを当てることができる。

style={({pressed}) => pressed && styles.pressed}
りゅうたりゅうた

ドロワーの実装

準備

パッケージをインストール

npm install @react-navigation/drawer

expoを使っていたら下記もインストール

npx expo install react-native-gesture-handler react-native-reanimated

雛形

import { createDrawerNavigator } from '@react-navigation/drawer';

const Drawer = createDrawerNavigator();

function MyDrawer() {
  return (
    <Drawer.Navigator>
      <Drawer.Screen name="Feed" component={Feed} />
      <Drawer.Screen name="Article" component={Article} />
    </Drawer.Navigator>
  );
}

DrawerItemのカスタマイズ

オプションでドロワーメニューのアイコンもカスタマイズできたりします。
詳しいオプションについてはこちら

<DrawerItem
   name="WelcomeScreen"
 options={{
  drawerlabel: 'WelcomScreen',
  drawerIcon: ({color, size}) => (
   <Ionicons name="home" color={color} size={18} />
  )
 }}
/>
りゅうたりゅうた

任意の文字やボタンでドロワーを開閉したい時

propsでnavgationを受け取りonPressでタップしたときにnavigation.toggleDrawer()を実行するようにすると可能

function UserScreen({ navigation }) {
 function openDrawerHandler() {
  navigation.toggleDrawer()
 }

 return(
  <Button title="Open Drawer" onPress={openDrawerHandler} />
 )
}
りゅうたりゅうた

下部タブ実装

import

npm install @react-navigation/bottom-tabs

雛形

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen
         name="Settings"
    component={SettingsScreen}
    options={{
    tabBarIcon: ({color, size}) => <Ionicons name="home" color={color} size={size}>
    }}
        />
    </Tab.Navigator>
  );
}
りゅうたりゅうた

ナビゲーションにドロワーをネストする

Stack.Navigatorの最初の子要素のStack.Screenのcomponentに関数を記述し、その関数内でDrawerを定義してアプリ開いた時に表示させたいコンポーネントを最初に持ってくる。

function Drawernavigator() {
  return (
    <Drawer.Navigator
      screenOptions={{
        headerStyle: {
          backgroundColor: "#351401",
        },
        headerTintColor: "white",
        sceneContainerStyle: { backgroundColor: "#3f3f25" },
        drawerContentStyle: { backgroundColor: "#351401" },
        drawerInactiveTintColor: "white",
        drawerActiveTintColor: "#e4baa1",
      }}
    >
      <Drawer.Screen
        name="Categories"
        component={CategoryScreen}
        options={{
          title: "All Categories",
          drawerIcon: ({ color, size }) => (
            <Ionicons name="list" color={color} size={size} />
          ),
        }}
      />
      <Drawer.Screen
        name="Favorites"
        component={FavoritesScreen}
        options={{
          drawerIcon: ({ color, size }) => (
            <Ionicons name="star" color={color} size={size} />
          ),
        }}
      />
    </Drawer.Navigator>
  );
}

export default function App() {
  return (
    <>
      <StatusBar style="light" />
      <NavigationContainer>
        <Stack.Navigator
          screenOptions={{
            headerStyle: {
              backgroundColor: "#351401",
            },
            headerTintColor: "white",
            contentStyle: { backgroundColor: "#3f3f25" },
          }}
        >
          <Stack.Screen
            name="Drawer"
            component={Drawernavigator}
            options={{
              title: "All Categories",
              headerShown: false,
            }}
          />
          <Stack.Screen name="MealsOverview" component={MealsOverviewScreen} />
          <Stack.Screen
            name="MealDetail"
            component={MealDetailScreen}
            options={{
              title: "About the Meal",
            }}
          />
        </Stack.Navigator>
      </NavigationContainer>
    </>
  );
}

これで実装するとheaderが二重になってしまうので、
Navigatorのoptionで headerShown: falseを指定すると非表示になる。

りゅうたりゅうた

コンテキストでstateをグローバルで管理

import { createContext, useState } from "react";

export const FavoritesContext = createContext({
  ids: [],
  addFavorite: (id) => {},
  removeFavorite: (id) => {},
});

function FavoritesContextProvider({children}) {
  const [favoriteMealIds, setFavoriteMealIds] =  useState([]);

  function addFavorite(id) {
    setFavoriteMealIds((currentFavIds) => [...currentFavIds, id] )
  }

  // 
  function removeFavorite(id) {
    setFavoriteMealIds((currentFavIds) => currentFavIds.filter(mealId => mealId !== id))
  }

  const value = {
    ids: favoriteMealIds,
    addFavorite:addFavorite,
    removeFavorite: removeFavorite
  }

  return <FavoritesContext.Provider value={value}>{children}</FavoritesContext.Provider>;
}

export default FavoritesContextProvider;

コンテキストの作成

export const FavoritesContext = createContext({
  ids: [],
  addFavorite: (id) => {},
  removeFavorite: (id) => {},
});

コンテキストの構造を定義します。ここではidsという配列とaddFavoriteとremoveFavoriteとして空の関数を設定

コンテキストプロバイダーの定義

function FavoritesContextProvider({ children }) {
  const [favoriteMealIds, setFavoriteMealIds] = useState([]);

アプリケーション全体の状態を更新するための状態を提供

追加と削除

引数のidから追加と削除を実装するための記述

  function addFavorite(id) {
    setFavoriteMealIds((currentFavIds) => [...currentFavIds, id] )
  }

  function removeFavorite(id) {
    setFavoriteMealIds((currentFavIds) => currentFavIds.filter(mealId => mealId !== id))
  }

コンテキストプロバイダーへの返却

valueオブジェクトにコンテキストプロバイダーに渡す値を定義し、FavoritesContext.Providerにvalueとして渡す

const value = {
    ids: favoriteMealIds,
    addFavorite:addFavorite,
    removeFavorite: removeFavorite
  }

  return <FavoritesContext.Provider value={value}>{children}</FavoritesContext.Provider>;
りゅうたりゅうた

クリックでモーダルを閉じる

propsのnavigatoinからgoBack関数を使えば閉じることができる。

  function cancelHandler() {
    navigation.goBack();
  }
りゅうたりゅうた

ローディング

ActivityIndicatorコンポーネントでデバイスに最適なローディングアニメーションを表示させる事ができる。

function LoadingOverlay() {
  return (
    <View style={styles.container}>
      <ActivityIndicator size={"large"} color={"white"} />
    </View>
  );
}

export default LoadingOverlay;
りゅうたりゅうた

imagePickerの実装

expoが用意しているimagePickerでカメラ機能を実装する事ができるのでメモ

インポート

npx expo install expo-image-picker

app.jsonで設定

{
  "expo": {
+    "plugins": [
+      [
+        "expo-image-picker",
+        {
+          "photosPermission": "The app accesses your photos to let you share them with your friends."
+        }
+      ]
+    ]
  }
}

実装

import {
  launchCameraAsync,
  useCameraPermissions,
  PermissionStatus,
} from "expo-image-picker";

function ImagePicker() {
  const [pickedImage, setPickedImage] = useState();
  const [cameraPermissionInformation, requestPermission] =
    useCameraPermissions();

  async function verifyPermissions() {
    if (cameraPermissionInformation.status === PermissionStatus.UNDETERMINED) {
      const permissionResponse = await requestPermission();
      return permissionResponse.granted;
    }

    // 拒否
    if (cameraPermissionInformation.status === PermissionStatus.DENIED) {
      Alert.alert("権限が不足しています。");
      return false;
    }

    return true;
  }

  async function takeImageHandler() {
    const hasPermission = await verifyPermissions();
    if (!hasPermission) {
      return;
    }
    const image = await launchCameraAsync({
      allowsEditing: true, // 写真を編集
      aspect: [16, 9], // アスペクト比を設定
      quality: 0.5, // 少し小さめの画像を取得
    });
    setPickedImage(image.assets[0].uri);

  }

  let imagePreview = <Text>画像はまだ撮影されていません。</Text>;

  if (imagePreview) {
    imagePreview = <Image style={styles.image} source={{ uri: pickedImage }} />;
  }

  return (
    <View>
      <View style={styles.imagePreview}>{imagePreview}</View>
      <OutlineButton icon={"camera"}  onPress={takeImageHandler}>take Image</OutlineButton>
    </View>
  );
}

useCameraPermissions

const [cameraPermissionInformation, requestPermission] = useCameraPermissions();
importしたuseCameraPermissionsから2つの値を取ります。
cameraPermissionInformation : カメラの権限に関する現在の情報をオブジェクトです。プロパティはstatusを持っており、PermissionStatus型の列挙型であり、下記のステータスを持っています。

Head Head
UNDETERMINED まだユーザーにカメラの権限を求めていない状態
DENIED ユーザーがカメラ権限のリクエストを拒否した状態
GRANTED ユーザーがカメラ権限のリクエストを許可した状態
りゅうたりゅうた

タップされているかどうかを真偽値判定

    <View style={styles.goalItem}>
      <Pressable
        onPress={() => onDeleteItem(itemData.item.id)}
        android_ripple={{ color: "#210644" }}
        style={({pressed}) => pressed && styles.pressedItem }
      >
        <Text style={styles.goalText}>{itemData.item.text}</Text>
      </Pressable>
    </View>
りゅうたりゅうた

スタイルでタップしているかどうかを真偽値判定

style属性の関数を渡すことでpressDataのpressedプロパティにアクセスできます。
これはタップした時にtrue, タップしていない時にfalseを返すという意味があります。
それによりスタイルを柔軟に管理することができます。
また、配列を渡しカンマ区切りで記述すると複数のスタイルを当てることもできます。

export default function PrimaryButton({ children }: PrimaryButtonProps) {
  function pressHandler() {
    console.log("Button pressed");
  }
  return (
    <View style={styles.buttonOuterContainer}>
      <Pressable
        style={({pressed}) => pressed ? [styles.buttonInputcontainer, styles.pressed] : styles.buttonInputcontainer}
        onPress={pressHandler}
        android_ripple={{ color: "#640233" }}
      >
        <Text style={styles.buttonText}>{children}</Text>
      </Pressable>
    </View>
  );
}