同じような機能を提供しているアプリなのに、一方は毎日開いてしまい、もう一方はインストールしたその日に消した——この差は、多くの場合「触った瞬間の印象」の差だと、私は考えています。
機能の豊富さでも、画面のデザインでもなく、ボタンを押した 0.1 秒の間に伝わってくる手触り。これが「良いアプリだな」と感じさせるか、「なんだか安っぽいな」と感じさせるかを決めています。
Rork は AI が画面を生成してくれる分、デザインや機能の見た目はすぐに整いますが、触ったときの手触りは AI 任せにすると平凡な結果になりがちです。ここではRork で作ったアプリの触り心地を底上げする3つの実装——アニメーション、ハプティクス、遷移速度——について、今日から使えるコードと一緒にお話しします。
アニメーションは「気持ちよさ」より「違和感のなさ」を狙う
アプリのアニメーションというと、派手で目を引くものを想像されるかもしれません。ですが、毎日使うアプリで大事なのは「気持ちよさ」よりも「違和感のなさ」です。
例えば、ボタンを押したときに反応がなさすぎるとユーザーは「押せていないのかな」と不安になり、逆に大げさに跳ね返ると「主張が強すぎて落ち着かないな」と感じてしまいます。理想は、ボタンが自分の指の動きに「ちょうど同じ速度でついてくる」感覚です。
タップフィードバックの基本実装
Rork で生成されたコンポーネントは、タップしても何も起きないか、もしくは TouchableOpacity による素朴な透明度変化で終わっていることが多いです。これを react-native-reanimated を使って、指に追従する自然な縮小に置き換えるだけで、印象はかなり変わります。
// components/PressableScale.tsx
// 用途: 指で押したときに 0.96 倍にスッと縮み、離すとゆるやかに戻るボタン
import React from "react";
import { Pressable, PressableProps } from "react-native";
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
} from "react-native-reanimated";
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export function PressableScale(props: PressableProps & { children: React.ReactNode }) {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
<AnimatedPressable
{...props}
onPressIn={(e) => {
// 押し込みは速めに(80ms)、違和感なく反応する
scale.value = withTiming(0.96, {
duration: 80,
easing: Easing.out(Easing.quad),
});
props.onPressIn?.(e);
}}
onPressOut={(e) => {
// 戻りは少しゆっくり(160ms)、残響感を残す
scale.value = withTiming(1, {
duration: 160,
easing: Easing.out(Easing.quad),
});
props.onPressOut?.(e);
}}
style={[animatedStyle, props.style as any]}
>
{props.children}
</AnimatedPressable>
);
}このコンポーネントをアプリ内の主要ボタン——特に「保存」「送信」「購入」といった重いアクションのボタン——に置き換えるだけで、アプリ全体の手触りが変わります。
コツは 押し込みと戻りの速度を非対称にする ことです。押し込みは指の動きについてくる速さ(80ms)、戻りはやや名残惜しさを残す速さ(160ms)。これが「反応が鈍い」と「うるさい」の間の、ちょうど良い範囲に収まります。
アニメーションをもう一歩踏み込みたい方は、Rork で Reanimated × Gesture Handler による高度なアニメーションを実装するガイド で、スプリング系イージングや複合ジェスチャを扱っています。
ハプティクスは「確定したとき」だけに絞る
iPhone の Taptic Engine、Android の振動モーターによる触覚フィードバック、いわゆるハプティクスは、使いどころを間違えると一気に安っぽくなる機能でもあります。
よくある失敗パターンは、「全てのタップに振動をつける」というものです。こうするとユーザーは数分でうるさく感じ、設定でバイブをオフにしてしまいます。ハプティクスは「ユーザーが何かを確定させた瞬間」にだけ使うのが原則です。
ハプティクスを鳴らすべき3つのタイミング
私がアプリを作るときにハプティクスを発火させるのは、以下の3つの瞬間に限定しています。
- 選択の確定: ピッカーやタブを切り替えたとき(選択が完了した実感を足す)
- 成功の通知: 保存や送信が完了したとき(目で見なくても完了が分かる)
- 不可逆な操作の前: 削除や購入を実行した瞬間(「今やったな」という実感を足す)
逆に、リストのスクロールや単純なボタン連打にはハプティクスを使いません。意識していない操作にまで触感をつけると、アプリ全体が騒がしくなるからです。
Expo Haptics での使い分け
Expo を使っている Rork プロジェクトなら、expo-haptics ですぐに導入できます。
// utils/haptics.ts
// 用途: 用途別のハプティクスを一箇所に集約して、使い方の統一ルールを守る
import * as Haptics from "expo-haptics";
export const hapticFeedback = {
// タブ切り替え・セグメントコントロール等(軽い選択音)
selection: () => Haptics.selectionAsync(),
// 保存完了・送信完了など成功系の通知
success: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success),
// 削除・購入など不可逆アクションの確定
impact: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium),
// 警告(入力エラー等) -- 使いすぎ注意。多用するとストレスになる
warning: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning),
};使う側はこのように書きます。
import { hapticFeedback } from "@/utils/haptics";
async function handleSave() {
await saveMemo();
hapticFeedback.success(); // 保存完了を指で感じられる
}重要なのは、一つの画面で鳴らすハプティクスの種類を2種類以下に絞る ことです。選択音と成功音を混ぜて使うと、どちらの意味もぼやけてしまいます。画面の中で「一番大事なアクション」にだけ impact や success を使い、それ以外は selection でそろえる、くらいのルールがちょうど良いです。
ハプティクス単体についてもっと詳しく試したい方は、Rork の Haptic Feedback 完全ガイド もあわせて読んでみてください。実装パターンを一通り並べてあります。
遷移速度は「詰まった感じ」をゼロにする
3つ目の要素は、ボタンをタップしてから画面が変わるまでの時間です。ここは最も見落とされがちで、最も効果の大きい改善ポイントです。
ユーザーは、タップしてから 100ms 以内に何らかの視覚変化があると「反応した」と感じ、300ms を超えると「詰まった」と感じると言われています。Rork で生成されたアプリを触ると、ここで 400〜600ms 待たされるケースが珍しくありません。
まず計測してから対処する
体感だけで「なんか遅い」と言っても改善は進みません。React Native では InteractionManager と console.time を組み合わせるだけで、遷移時間の実測ができます。
// hooks/useTransitionTimer.ts
// 用途: タップ→画面表示までの実測時間をログに出す(開発時のみ有効)
import { useEffect } from "react";
import { InteractionManager } from "react-native";
export function useTransitionTimer(label: string) {
useEffect(() => {
const start = Date.now();
const handle = InteractionManager.runAfterInteractions(() => {
const duration = Date.now() - start;
if (__DEV__) console.log(`[transition] ${label}: ${duration}ms`);
});
return () => handle.cancel();
}, [label]);
}画面コンポーネントの先頭でこのフックを呼んで、アプリ内の主要な画面遷移について「何ミリ秒かかっているか」を書き出してみてください。多くの場合、遅延の原因は3つに絞られます。
- 初回レンダリング時の同期的な API 通信: データ取得完了を待ってから画面を描画している
- 大きな画像のロード: ネットワーク越しに画像を取得してから画面を描画している
- 重い State 計算: リスト全体の map/filter を render の中で毎回実行している
解決の方向: データ取得と画面表示を分離する
原則は「先に画面の枠を出して、データは後から差し込む」です。
// screens/FeedScreen.tsx
// 用途: 枠(skelton) を即座に表示し、データを裏で取得してから中身を差し替える
import { useQuery } from "@tanstack/react-query";
import { FlatList, View } from "react-native";
import { SkeletonRow } from "@/components/SkeletonRow";
import { FeedRow } from "@/components/FeedRow";
export function FeedScreen() {
const { data, isLoading } = useQuery({
queryKey: ["feed"],
queryFn: fetchFeed,
staleTime: 60_000, // 1 分間はキャッシュを信用する(再入時の再取得を抑える)
});
if (isLoading) {
// ローディング表示は最低 6 行のスケルトンで、空白を作らない
return (
<View>
{Array.from({ length: 6 }).map((_, i) => (
<SkeletonRow key={i} />
))}
</View>
);
}
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <FeedRow item={item} />}
initialNumToRender={8} // 初回はファーストビュー分だけレンダリング
windowSize={5} // リストのスクロール範囲外は間引いて計算コストを抑える
/>
);
}ポイントは「空のスピナーを出さない」ことです。空白のくるくるを見せるより、スケルトン(灰色の四角の列)を先に出したほうが、体感速度は明確に速く感じられます。目が何かしらの形を認識した瞬間から、ユーザーの中では画面が表示されたと解釈されるためです。
画面遷移そのものの最適化については、Rork アプリの起動時間を速くするための実践ガイド で、スプラッシュの最小化やアセットのプリロードについても触れています。
Before / After: 同じ画面でもここまで変わる
最後に、3つの改善を一画面に適用したときの具体的な違いを言葉で描写しておきます。
Before(Rork が生成した初期状態):
- ボタンを押すと、わずかに透明度が下がるだけで、指の動きに反応が鈍い
- 保存ボタンを押しても振動もなく、成功したのかどうか画面を目で追う必要がある
- 保存後に次の画面へ行くとき、スピナーだけが回る画面を 500ms ほど眺めさせられる
After(3つの実装を適用):
- ボタンがスッと 0.96 倍に縮んで指に追従し、押した手応えが伝わる
- 保存直後に軽い「トン」という触感があり、目で確認しなくても成功したと分かる
- 次の画面はスケルトンで即座に出て、その間にデータが裏側で埋まっていく
同じ機能、同じ配色、同じレイアウトのまま、この3つだけを実装し直すと「このアプリ、なんか気持ちいいな」という感想が返ってくるようになります。私が運用しているアプリでも、これらを反映した直後から、ストアレビューに「操作感が良い」「サクサク動く」というコメントが増えました。
触り心地をさらに体系的に
全体を振り返って — 今日試せる1つのこと
この記事で紹介した3つの改善のうち、一度に全部を入れる必要はありません。まずはアプリの中で一番よく押されるボタン、たった1つだけで、PressableScale と hapticFeedback.success を組み合わせてみてください。
「一番よく押されるボタン」はアプリによって違いますが、メモアプリなら保存ボタン、EC アプリならカート追加ボタン、SNS アプリなら投稿ボタンです。そこが気持ちよく動くようになると、アプリ全体の印象が一段階変わります。実測時間は、10分もあれば終わります。
AI に画面を作ってもらう時代だからこそ、最後の触り心地の調整は、自分の手で丁寧に仕上げる価値のある作業です。1つのボタンから始めて、アプリ全体の手触りを少しずつ整えていただけたら嬉しいです。