コース目次 / 第3章

成果物 — 旅先ルーレット

ボタンを押すと行き先が1つ選ばれる、動くアプリを作ります。

第3章 / 全5章目安 約60分この章のゴール: スマホのボタンをタップすると、行き先が変わる
お使いのエディタ
ファイル操作が切り替わります

いよいよ道具を作ります。ボタンを押すと、次の行き先が1つ選ばれるアプリです。

今日のゴール

スマホの画面にボタンが1つ。押すと、行き先の名前と一言が入れ替わる。それだけです。それだけですが、あなたのスマホで動きます。

1. 選ぶ処理を、画面から切り離す

先に lib というフォルダを作り、その中に roulette.ts を作ります。

ターミナル派の方

VS Codeのターミナルターミナル
mkdir libnvim lib/roulette.ts

行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。

エディタは nvim でなくて構いません。お好みのエディタのコマンドに読み替えてください。

lib/roulette.ts
export type Spot = {
  name: string;
  hint: string;
};

export const SPOTS: Spot[] = [
  { name: '直島', hint: '港からレンタサイクルで一周' },
  { name: '尾道', hint: '坂の途中に猫がたくさん' },
  { name: '金沢', hint: '雨の日の石畳がきれい' },
  { name: '松本', hint: '夕方の城といえばここ' },
  { name: '函館', hint: '朝市で朝ごはんから' },
  { name: '高知', hint: '日曜市を端から端まで' },
];

/**
 * 候補から1つ選ぶ。
 *
 * random を外から渡せるようにしてあるのは、同じ答えを何度でも出せるように
 * するため。ふだんは省略して Math.random が使われる。
 */
export function pick(spots: Spot[], random: () => number = Math.random): Spot {
  return spots[Math.floor(random() * spots.length)];
}

なぜ画面(App.tsx)と別のファイルにするのか。「どれを選ぶか」は画面が無くても確かめられる処理だからです。分けておくと、あとで見直すときに画面の都合と混ざりません。
Tsumiki のこの教材でも、この roulette.ts だけは公開前に機械で走らせて確かめています(画面は人が実機で見るしかありませんが、選び方は自動で検査できます)。

random を引数で受け取っているところが、少し変わって見えるかもしれません。ふだんは省略して Math.random が使われますが、渡せるようにしておくと同じ答えを何度でも出せます。検査のためであり、説明用のスクリーンショットを撮り直せるようにするためでもあります。

Spot という型を作りました。コース2でやった「形に名前を付ける」やつです。ここから先、spot.nmae のような打ち間違いは赤い波線が教えてくれます。

2. 画面を作る

App.tsx を、まるごとこう書き換えます。

App.tsx
import { StatusBar } from 'expo-status-bar';
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { pick, SPOTS, type Spot } from './lib/roulette';

export default function App() {
  const [spot, setSpot] = useState<Spot | null>(null);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>旅先ルーレット</Text>
      <Text style={styles.lead}>はるとの、次の行き先を決める道具</Text>

      <View style={styles.result}>
        {spot === null ? (
          <Text style={styles.waiting}>ボタンを押してください</Text>
        ) : (
          <>
            <Text style={styles.name}>{spot.name}</Text>
            <Text style={styles.hint}>{spot.hint}</Text>
          </>
        )}
      </View>

      <Pressable style={styles.button} onPress={() => setSpot(pick(SPOTS))}>
        <Text style={styles.buttonLabel}>まわす</Text>
      </Pressable>

      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fbf8f4',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#2f2a26',
  },
  lead: {
    marginTop: 8,
    fontSize: 15,
    color: '#6b6058',
  },
  result: {
    height: 140,
    alignItems: 'center',
    justifyContent: 'center',
  },
  waiting: {
    fontSize: 15,
    color: '#a1968c',
  },
  name: {
    fontSize: 44,
    fontWeight: 'bold',
    color: '#8a6a4f',
  },
  hint: {
    marginTop: 10,
    fontSize: 15,
    color: '#6b6058',
  },
  button: {
    backgroundColor: '#8a6a4f',
    paddingVertical: 16,
    paddingHorizontal: 48,
    borderRadius: 999,
  },
  buttonLabel: {
    color: '#ffffff',
    fontSize: 18,
    fontWeight: 'bold',
  },
});

長く見えますが、下半分は全部見た目の設定です。動きに関わるのは上の30行だけです。

3. ミニ解説: 3つの新しいもの

書き方 何をするか
useState<Spot | null>(null) いま何が選ばれているかを覚えておく箱。最初は「まだ何も」の意味で null
<Pressable onPress={...}> 押せる部品。押されたら中の処理を走らせる
{spot === null ? A : B} まだ押していないときはA、押したあとはBを出す(コース2でやった三項演算子です)

setSpot(...) を呼ぶと、画面が描き直されます。自分で書き換える処理は1行も書いていません。ここがReactの世界のやり方で、試食章で触った useState と同じものです。

<> と </> という空の札が出てきました。「2つの部品をまとめて1つとして扱う」ための、名前のない箱です。名前も見た目も持たないので、画面には何の影響もありません。

4. スマホで押してみる

保存すると、スマホの表示が変わります。ボタンを押してください。

行き先が出ます。もう一度押すと、別のものに変わります。

ときどき同じものが続けて出ます。6件から毎回でたらめに選んでいるので、当然そうなります。これは第4章で直します。「壊れているように見えるものは、壊れていなくても直す」というのも、道具を作るということの一部です。

こわしてみる

pick(SPOTS) を pick(SPOTS, () => 0) に書き換えて保存してください。

何度押しても 直島 しか出なくなります。 random に「いつも0を返す関数」を渡したので、いつも先頭が選ばれます。

これが、さっき「同じ答えを何度でも出せる」と書いたことの正体です。確かめたら、元の pick(SPOTS) に戻してください。

ハマったら

「Unable to resolve module ./lib/roulette」と出る

ファイルの置き場所が違います。lib フォルダは App.tsx と同じ高さに置いてください。VS Code の左側で、App.tsx と lib が同じ位置に並んでいれば正解です。ls を打って、App.tsx と lib が両方出てくれば正解です。

ボタンを押しても何も起こらない

onPress の中が setSpot(pick(SPOTS)) になっているか確かめてください。onPress={setSpot(pick(SPOTS))} のように () => が抜けていると、押される前に1回だけ実行されて終わりになります。

ZIPで戻したい

このページの下のZIPは、この章を終えた状態のフォルダです。解凍して、その中で作業を続けられます。自分で書き換えた部分は消えてしまうので、先に自分のファイルを別の場所へ待避してから使ってください。

こうなっていればOK

うまくいかないときは、この章の完成ファイル一式(ZIP)をダウンロードして続きから進めても大丈夫です。

卒業まであと1章です。

この章はまだ完了していません。