コース目次 / 第3章
成果物 — 旅先ルーレット
ボタンを押すと行き先が1つ選ばれる、動くアプリを作ります。
いよいよ道具を作ります。ボタンを押すと、次の行き先が1つ選ばれるアプリです。
今日のゴール
スマホの画面にボタンが1つ。押すと、行き先の名前と一言が入れ替わる。それだけです。それだけですが、あなたのスマホで動きます。
1. 選ぶ処理を、画面から切り離す
先に lib というフォルダを作り、その中に roulette.ts を作ります。
ターミナル派の方
行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。
エディタは nvim でなくて構いません。お好みのエディタのコマンドに読み替えてください。
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 を、まるごとこう書き換えます。
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章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。