Open18

Expoチュートリアルやるだけ

S-guchiS-guchi

チュートリアルどおりセットアップする

npx create-expo-app@latest StickerSmash
cd StickerSmash
StickerSmash git:(master) tree -L 2 -I "node_modules"
.
├── README.md
├── app
│   ├── (tabs)
│   ├── +not-found.tsx
│   └── _layout.tsx
├── app.json
├── assets
│   ├── fonts
│   └── images
├── components
│   ├── Collapsible.tsx
│   ├── ExternalLink.tsx
│   ├── HapticTab.tsx
│   ├── HelloWave.tsx
│   ├── ParallaxScrollView.tsx
│   ├── ThemedText.tsx
│   ├── ThemedView.tsx
│   └── ui
├── constants
│   └── Colors.ts
├── eslint.config.js
├── hooks
│   ├── useColorScheme.ts
│   ├── useColorScheme.web.ts
│   └── useThemeColor.ts
├── package-lock.json
├── package.json
├── scripts
│   └── reset-project.js
└── tsconfig.json

公式が用意してるassetsをダウンロードして
assets/imagesに入れた。

名前が被ってるものは何も考えず置換した

S-guchiS-guchi

reset-project スクリプトを実行

最低限のものだけ残してあとはapp-exampleにいったん退避するスクリプトらしい。
最初はこれ叩いて後から必要なものをapp-exampleからコピーしてくるといいよってことかな?

npm run reset-project

> stickersmash@1.0.0 reset-project
> node ./scripts/reset-project.js

Do you want to move existing files to /app-example instead of deleting them? (Y/n): Y
📁 /app-example directory created.
➡️ /app moved to /app-example/app.
➡️ /components moved to /app-example/components.
➡️ /hooks moved to /app-example/hooks.
➡️ /constants moved to /app-example/constants.
➡️ /scripts moved to /app-example/scripts.

📁 New /app directory created.
📄 app/index.tsx created.
📄 app/_layout.tsx created.

✅ Project reset complete. Next steps:
1. Run `npx expo start` to start a development server.
2. Edit app/index.tsx to edit the main screen.
3. Delete the /app-example directory when you're done referencing it.

すっきりした

tree -L 2 -I "node_modules"
.
├── README.md
├── app
│   ├── _layout.tsx
│   └── index.tsx
├── app-example
│   ├── app
│   ├── components
│   ├── constants
│   ├── hooks
│   └── scripts
├── app.json
├── assets
│   ├── fonts
│   └── images
├── eslint.config.js
├── package-lock.json
├── package.json
└── tsconfig.json

11 directories, 8 files
S-guchiS-guchi

Expo GOでアプリ起動

npx expo start

コマンド表が出てくる
iでios
aでandroid

それぞれ10秒ほどで起動した。早い!

S-guchiS-guchi

チュートリアルのコードペタって貼ってみる。
cssのようにかける

import { Text, View,  StyleSheet } from 'react-native';

export default function Index() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Home screen</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    color: '#fff',
  },
});

S-guchiS-guchi

スタックに新しいスクリーンを追加する

import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Stack.Screen name="about" options={{ title: 'About' }} />
    </Stack>
  );
}

このスタックって何?
StackにStack.Screen入れてるね

スタックナビゲータは、アプリ内の異なる画面間を移動するための基盤です。Androidでは、スタックルートは現在の画面の上でアニメーションします。iOSでは、スタックルートは右からアニメーションします。Expo Routerには、新しいルートを追加するためのナビゲーション スタックを作成するStackコンポーネントが用意されています。

???
画面を積み重ねる、みたいな感じかな。
Stackコンポーネントを積み重ねていくのかな

S-guchiS-guchi
import { Link } from "expo-router";
import { StyleSheet, Text, View } from "react-native";

export default function Index() {
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Home screen</Text>
      <Link href="/about" style={styles.button}>
        リンクボタンだよ
      </Link>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#25292e",
    alignItems: "center",
    justifyContent: "center",
  },
  text: {
    color: "#fff",
  },
  button: {
    fontSize: 20,
    textDecorationLine: "underline",
    color: "#fff",
  },
});


S-guchiS-guchi

+not-found.tsxってファイルを
appの下に作っておくと存在しないページを開いちゃった時このファイルを表示してくれる。

これは必須だ!

import { Link, Stack } from "expo-router";
import { StyleSheet, View } from "react-native";

export default function NotFoundScreen() {
  return (
    <>
      <Stack.Screen options={{ title: "お探しのページは見つかりませんでした" }} />
      <View style={styles.container}>
        <Link href="/" style={styles.button}>
          トップページに戻る
        </Link>
      </View>
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#25292e",
    justifyContent: "center",
    alignItems: "center",
  },

  button: {
    fontSize: 20,
    textDecorationLine: "underline",
    color: "#fff",
  },
});

S-guchiS-guchi
tree app -L 2 -I "node_modules"
app
├── (tabs)
│   ├── _layout.tsx
│   ├── about.tsx
│   └── index.tsx
├── +not-found.tsx
└── _layout.tsx

app/(tabs)/_layout.tsx

import { Tabs } from "expo-router";

export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" options={{ title: "Home" }} />
      <Tabs.Screen name="about" options={{ title: "About" }} />
    </Tabs>
  );
}

app/_layout.tsx

import { Stack } from "expo-router";

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
    </Stack>
  );
}

<Tabs>で タブができる
<Stack>はそのTabsを管理する

options={{ headerShown: false }}

をつけないと以下のような感じになる。

S-guchiS-guchi

app/(tabs)/_layout.tsx

import { Tabs } from "expo-router";

import Ionicons from "@expo/vector-icons/Ionicons";

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: "#ffd33d",
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: ({ color, focused }) => <Ionicons name={focused ? "home-sharp" : "home-outline"} color={color} size={24} />,
        }}
      />
      <Tabs.Screen
        name="about"
        options={{
          title: "About",
          tabBarIcon: ({ color, focused }) => (
            <Ionicons name={focused ? "information-circle" : "information-circle-outline"} color={color} size={24} />
          ),
        }}
      />
    </Tabs>
  );
}

screenOptionsとかoptionでオシャレにできる。

S-guchiS-guchi

画像を表示する

画像を表示するやつをインストール

✗ npx expo install expo-image
› Installing 1 SDK 53.0.0 compatible native module using npm
> npm install

added 1 package, and audited 942 packages in 910ms

174 packages are looking for funding
  run `npm fund` for details

npm installとexpo installは何が違うんだろう

→ expo installでインストールすると現在のSDKのバージョンにあったライブラリをインストールすることができる。なので特に理由なければこれ使う

S-guchiS-guchi
import { Image } from "expo-image";
import { StyleSheet, View } from "react-native";

const PlaceholderImage = require("@/assets/images/background-image.png");

export default function Index() {
  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <Image source={PlaceholderImage} style={styles.image} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#25292e",
    alignItems: "center",
  },
  imageContainer: {
    flex: 1,
  },
  image: {
    width: 320,
    height: 440,
    borderRadius: 18,
  },
});

表示できた。ほんとwebっぽい

S-guchiS-guchi

コンポーネントに分ける
components/ImageViewer.tsx

import { Image } from "expo-image";
import { ImageSourcePropType, StyleSheet } from "react-native";

type Props = {
  imgSource: ImageSourcePropType;
};

export default function ImageViewer({ imgSource }: Props) {
  return <Image source={imgSource} style={styles.image} />;
}

const styles = StyleSheet.create({
  image: {
    width: 320,
    height: 440,
    borderRadius: 18,
  },
});

app/(tabs)/index.tsx

import { StyleSheet, View } from "react-native";

import ImageViewer from "@/components/ImageViewer";

const PlaceholderImage = require("@/assets/images/background-image.png");

export default function Index() {
  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#25292e",
    alignItems: "center",
  },
  imageContainer: {
    flex: 1,
  },
});

appの下にcomponentsなどのルーティングに関係ないディレクトリやファイルは作ってはならんらしい。

All screens/pages are files inside of app directory

https://docs.expo.dev/router/basics/core-concepts/#5-non-navigation-components-live-outside-of-app-directory

S-guchiS-guchi

ボタンつける
components/button.tsx

import { StyleSheet, View, Pressable, Text } from 'react-native';

type Props = {
  label: string;
};

export default function Button({ label }: Props) {
  return (
    <View style={styles.buttonContainer}>
      <Pressable style={styles.button} onPress={() => alert('You pressed a button.')}>
        <Text style={styles.buttonLabel}>{label}</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  buttonContainer: {
    width: 320,
    height: 68,
    marginHorizontal: 20,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 3,
  },
  button: {
    borderRadius: 10,
    width: '100%',
    height: '100%',
    alignItems: 'center',
    justifyContent: 'center',
    flexDirection: 'row',
  },
  buttonLabel: {
    color: '#fff',
    fontSize: 16,
  },
});

app/(tabs)/index.tsx

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

import Button from '@/components/Button'; 
import ImageViewer from '@/components/ImageViewer';

const PlaceholderImage = require("@/assets/images/background-image.png");

export default function Index() {
  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} />
      </View>
      <View style={styles.footerContainer}>
        <Button label="Choose a photo" />
        <Button label="Use this photo" />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
  },
  imageContainer: {
    flex: 1,
    paddingTop: 28,
  },
  footerContainer: {
    flex: 1 / 3,
    alignItems: 'center',
  },
});

なんか被ってるけどまあいいか。。笑

<Pressable>を使うのがおすすめらしい

S-guchiS-guchi

ボタンコンポーネントにthemeの引数を追加してデザインを分けるようにする。

この時、コード変更後なぜかボタンにデザインが入らなくて困った。
Expoを再起動したら見栄えがチュートリアルと同じになったので、よくわからないこと起きたら
再起動試すのもありかも

Expo起動してるコンソールでr押せばリロードできる

import { StyleSheet, View } from "react-native";

import Button from "@/components/button";
import ImageViewer from "@/components/ImageViewer";

const PlaceholderImage = require("@/assets/images/background-image.png");

export default function Index() {
  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} />
      </View>
      <View style={styles.footerContainer}>
        <Button theme="primary" label="Choose a photo" />
        <Button label="Use this photo" />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#25292e",
    alignItems: "center",
  },
  imageContainer: {
    flex: 1,
  },
  footerContainer: {
    flex: 1 / 3,
    alignItems: "center",
  },
});


import FontAwesome from "@expo/vector-icons/FontAwesome";
import { Pressable, StyleSheet, Text, View } from "react-native";

type Props = {
  label: string;
  theme?: "primary";
};

export default function Button({ label, theme }: Props) {
  if (theme === "primary") {
    return (
      <View style={[styles.buttonContainer, { borderWidth: 4, borderColor: "#ffd33d", borderRadius: 18 }]}>
        <Pressable style={[styles.button, { backgroundColor: "#fff" }]} onPress={() => alert("You pressed a button.")}>
          <FontAwesome name="picture-o" size={18} color="#25292e" style={styles.buttonIcon} />
          <Text style={[styles.buttonLabel, { color: "#25292e" }]}>{label}</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <View style={styles.buttonContainer}>
      <Pressable style={styles.button} onPress={() => alert("You pressed a button.")}>
        <Text style={styles.buttonLabel}>{label}</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  buttonContainer: {
    width: 320,
    height: 68,
    marginHorizontal: 20,
    alignItems: "center",
    justifyContent: "center",
    padding: 3,
  },
  button: {
    borderRadius: 10,
    width: "100%",
    height: "100%",
    alignItems: "center",
    justifyContent: "center",
    flexDirection: "row",
  },
  buttonIcon: {
    paddingRight: 8,
  },
  buttonLabel: {
    color: "#fff",
    fontSize: 16,
  },
});

S-guchiS-guchi

画像ピッカーのインスト

npx expo install expo-image-picker

※ 開発サーバー落としてからインストールせよとのこと(さっき落とさずインストールしたかも、そのせいでExpoの挙動がおかしかった説)

S-guchiS-guchi

ボタンコンポーネントにonpress追加

type Props = {
  label: string;
  theme?: "primary";
  onPress: () => void;
};

export default function Button({ label, theme, onPress }: Props) {

pickImageAsync関数追加

import { View, StyleSheet } from 'react-native';
import * as ImagePicker from 'expo-image-picker';

import Button from '@/components/Button';
import ImageViewer from '@/components/ImageViewer';

const PlaceholderImage = require('@/assets/images/background-image.png');

export default function Index() {
  const pickImageAsync = async () => {
    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ['images'],
      allowsEditing: true,
      quality: 1,
    });

    if (!result.canceled) {
      console.log(result);
    } else {
      alert('You did not select any image.');
    }
  };

  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} />
      </View>
      <View style={styles.footerContainer}>
        <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} />
        <Button label="Use this photo" />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
  },
  imageContainer: {
    flex: 1,
  },
  footerContainer: {
    flex: 1 / 3,
    alignItems: 'center',
  },
});

画像選択出せる、簡単!

S-guchiS-guchi

あとは選択した画像のurlを保存するuseState作って、
それをImageViewrに渡す感じにして終わり

import { View, StyleSheet } from 'react-native';
import * as ImagePicker from 'expo-image-picker';

import { useState } from 'react';


import Button from '@/components/Button';
import ImageViewer from '@/components/ImageViewer';

const PlaceholderImage = require('@/assets/images/background-image.png');

export default function Index() {
  const [selectedImage, setSelectedImage] = useState<string | undefined>(undefined);

  const pickImageAsync = async () => {
    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ['images'],
      allowsEditing: true,
      quality: 1,
    });

    if (!result.canceled) {
      setSelectedImage(result.assets[0].uri);
    } else {
      alert('You did not select any image.');
    }
  };

  return (
    <View style={styles.container}>
      <View style={styles.imageContainer}>
        <ImageViewer imgSource={PlaceholderImage} selectedImage={selectedImage} />
      </View>
      <View style={styles.footerContainer}>
        <Button theme="primary" label="Choose a photo" onPress={pickImageAsync} />
        <Button label="Use this photo" />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#25292e',
    alignItems: 'center',
  },
  imageContainer: {
    flex: 1,
  },
  footerContainer: {
    flex: 1 / 3,
    alignItems: 'center',
  },
});

import { ImageSourcePropType, StyleSheet } from 'react-native';
import { Image } from 'expo-image';

type Props = {
  imgSource: ImageSourcePropType;
  selectedImage?: string;
};

export default function ImageViewer({ imgSource, selectedImage }: Props) {
  const imageSource = selectedImage ? { uri: selectedImage } : imgSource;

  return <Image source={imageSource} style={styles.image} />;
}

const styles = StyleSheet.create({
  image: {
    width: 320,
    height: 440,
    borderRadius: 18,
  },
});