RORK LABEN
EVENT — 本日9月9日、Apple が「Surprise and Shine」と題したイベントを開きます。日本時間では9月10日の未明2時からですEXPECT — iPhone 18 Pro と Pro Max、折りたたみ機、2nm プロセスの A20 Pro チップ、そして iOS 27 以下の配信日発表が見込まれていますWAIT — この記事を書いている時点ではまだ開催前です。噂の段階で書いたものと発表後に書いたものが混ざると、読む側には区別がつきませんMAX — Rork Max がネイティブ Swift を生成する以上、Apple の動きは他人事ではありません。標準の Rork は React Native、という線引きは繰り返し確認したいところですSIMULATOR — Rork Max はクラウド上の Mac でコンパイルし、ブラウザ内で動くストリーミングの iOS シミュレータで確認できます。Xcode も Mac の実機も要りませんSEASON — OS が新しくなる時期は、自動生成の足場がいちばん揺れます。便利さを謳う記事ほど、この揺れに触れないと不誠実になると感じていますEVENT — 本日9月9日、Apple が「Surprise and Shine」と題したイベントを開きます。日本時間では9月10日の未明2時からですEXPECT — iPhone 18 Pro と Pro Max、折りたたみ機、2nm プロセスの A20 Pro チップ、そして iOS 27 以下の配信日発表が見込まれていますWAIT — この記事を書いている時点ではまだ開催前です。噂の段階で書いたものと発表後に書いたものが混ざると、読む側には区別がつきませんMAX — Rork Max がネイティブ Swift を生成する以上、Apple の動きは他人事ではありません。標準の Rork は React Native、という線引きは繰り返し確認したいところですSIMULATOR — Rork Max はクラウド上の Mac でコンパイルし、ブラウザ内で動くストリーミングの iOS シミュレータで確認できます。Xcode も Mac の実機も要りませんSEASON — OS が新しくなる時期は、自動生成の足場がいちばん揺れます。便利さを謳う記事ほど、この揺れに触れないと不誠実になると感じています
記事一覧/開発ツール
開発ツール/2026-08-24中級

ダークモードの確認を DevTools に寄せて、実機で見るべき場所が4か所に減りました

Expo SDK 57 の React Native DevTools に入ったライト/ダークのエミュレーションで、端末設定の往復をやめました。差し替わるのは JS が報告する値だけで、実機でしか確認できない場所が残ります。その境界を1画面で切り分ける方法をまとめました。

Expo203SDK 57ダークモードReact Native236DevTools

朝の30分を、設定アプリの往復に溶かしていました。

一覧画面を暗くして確認、設定を開いてライトに戻す、詳細画面を暗くして確認、また戻す。壁紙アプリのように画面のほとんどが画像で埋まるアプリだと、背景色の差が出る場所は少ないのに、確認そのものは全画面ぶん必要になります。1画面あたり10秒の切り替えでも、20画面で往復すれば7分近くが移動時間です。個人開発で本数を抱えていると、この7分がアプリの本数ぶん積み上がります。

Expo SDK 57 に上げたあと、React Native DevTools にライトモードとダークモードをエミュレートする切り替えが入っていることに気づきました。端末の設定を触らずにトグル1つで切り替わります。

正直に言えば、最初は「これで全部片づく」と思いました。実際には片づかない場所が残りました。ただ、残った場所がどこかを一度きちんと線引きしたことで、確認の手順がかなり短くなりました。その線引きを書き残しておきます。

エミュレーションが差し替えているのは「JS が報告する値」です

DevTools のトグルが何をしているのかを理解しておくと、あとの判断が速くなります。

React Native には Appearance モジュールがあり、Appearance.setColorScheme() で JS 側が報告する配色を上書きできます。DevTools のエミュレーションは、この上書きを開発ツールから叩けるようにしたものと考えると筋が通ります。

つまり、useColorScheme() の戻り値を見て色を決めているコンポーネントは、すべて切り替わります。

import { useColorScheme, View, Text } from 'react-native';
 
export function Card({ title }: { title: string }) {
  const scheme = useColorScheme(); // 'light' | 'dark' | null
  const isDark = scheme === 'dark';
 
  return (
    <View style={{ backgroundColor: isDark ? '#16181d' : '#ffffff' }}>
      <Text style={{ color: isDark ? '#e8eaed' : '#1f2328' }}>{title}</Text>
    </View>
  );
}

このコードは DevTools のトグルで完全に追従します。NativeWind の dark: 修飾子も、内部で同じ配色ソースを読んでいるので同様です。

逆に言えば、OS の配色設定をネイティブ側で直接読んでいるものは追従しません。JS が「今はダークです」と申告しても、UIKit や Android のリソース解決は元の端末設定のまま動き続けます。

ここが境界です。手元で最初に混乱したのも、この2つが同じ画面に同居していたからでした。

出所を1画面に並べて、自分のプロジェクトで境界を確かめる

仕組みの説明を信じるより、自分のプロジェクトとバージョンで一度確かめたほうが確実です。配色の申告先を3つ並べて表示するだけの診断画面を作りました。

import { useEffect, useState } from 'react';
import { Appearance, useColorScheme, View, Text, Platform } from 'react-native';
import { WebView } from 'react-native-webview';
 
const PROBE_HTML = `
<html><body style="margin:0;font:16px -apple-system,system-ui">
  <div id="out" style="padding:12px"></div>
  <script>
    const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    document.getElementById('out').textContent = 'WebView: ' + (dark ? 'dark' : 'light');
    document.body.style.background = dark ? '#16181d' : '#ffffff';
    document.body.style.color = dark ? '#e8eaed' : '#1f2328';
  </script>
</body></html>`;
 
export function ColorSchemeProbe() {
  const hookScheme = useColorScheme();
  const [snapshot, setSnapshot] = useState(Appearance.getColorScheme());
 
  useEffect(() => {
    const sub = Appearance.addChangeListener(({ colorScheme }) => {
      setSnapshot(colorScheme);
    });
    return () => sub.remove();
  }, []);
 
  return (
    <View style={{ gap: 8, padding: 16 }}>
      <Text>useColorScheme(): {String(hookScheme)}</Text>
      <Text>Appearance.getColorScheme(): {String(snapshot)}</Text>
      <Text>Platform: {Platform.OS}</Text>
      <WebView source={{ html: PROBE_HTML }} style={{ height: 64 }} />
    </View>
  );
}

この画面を開いたまま DevTools のトグルを操作します。上の2行が切り替わり、WebView の行が動かなければ、境界は想定どおりです。もし WebView まで追従したなら、そのバージョンでは私の前提が古いということですので、以降の4か所は改めて確認してください。

確認は1分で終わりますし、SDK を上げるたびに走らせる価値があります。バージョン間の挙動差を人づてに信じるより、手元の1画面のほうが速くて確実です。

エミュレーションでは動かない4か所

診断画面で境界を確認したうえで、実機の設定を切り替えないと見えない場所を整理しました。

場所配色の決まり方確認の手段
スプラッシュ画面 アプリ起動前に OS がネイティブリソースから描画 端末設定を切り替えてコールドスタート
ネイティブアラート・キーボード UIKit / Android のトレイトに従う 端末設定を切り替えて該当画面を開く
WebView 内の prefers-color-scheme WebView 自身が OS 設定を参照 端末設定の切り替え、または明示的な注入
Android のシステムバー背景 styles.xml と edge-to-edge の設定 端末設定を切り替えて実機で目視

このうち WebView は明示的に渡してしまうのが実務的でした。OS の設定に任せると、アプリ本体だけがダークで WebView だけが白い、という不揃いが起きます。利用規約やヘルプを WebView で出しているアプリでは目立ちます。

const scheme = useColorScheme() ?? 'light';
 
<WebView
  source={{ uri: 'https://example.com/help' }}
  injectedJavaScriptBeforeContentLoaded={`
    document.documentElement.dataset.scheme = ${JSON.stringify(scheme)};
    true;
  `}
/>

受け側の CSS を prefers-color-scheme ではなく [data-scheme="dark"] で書いておけば、アプリ側の配色ソース1つに統一できます。DevTools のトグルでも追従するようになるので、確認漏れの箇所そのものが1つ減ります。

ネイティブのキーボードは、少しだけ手当ができます。TextInputkeyboardAppearance を配色ソースから渡しておけば、iOS のキーボードだけは JS 側の判断に寄せられます。

const scheme = useColorScheme() ?? 'light';
 
<TextInput
  placeholder="検索"
  keyboardAppearance={scheme === 'dark' ? 'dark' : 'light'}
/>

検索欄のあるアプリでは、ここが揃っていないと入力のたびに違和感が出ます。逆に Alert.alert() のようなネイティブのダイアログには同種の指定がありませんので、そこは実機の切り替えで見るしかありません。見る対象を「直せるもの」と「見るしかないもの」に分けておくと、確認の時間配分が決めやすくなります。

スプラッシュ画面については、そもそも配色を分けない選択も現実的です。ブランドカラー1色で塗ってしまえば、ライトとダークの差を確認する対象から外れます。私は壁紙アプリでこの方針を取っていて、起動直後のちらつきに悩む時間がなくなりました。

Android のシステムバーは、SDK 57 に edge-to-edge まわりの修正が入っているため、上げた直後に一度は実機で見ておくことをおすすめします。ネイティブ設定を触っている場合は、Expo SDK 57 に上げる前に、prebuild で消えるネイティブ変更を洗い出すのほうを先に済ませておくと、原因の切り分けが楽になります。

リリース前の確認手順を書き直しました

線引きが済んだあと、App Store と Google Play へ提出する前に必ず通す手順として、こう固定しました。

DevTools でまとめて見る(全画面・切り替えは1回)

アプリを起動したまま、トグルをダークにして全画面を通します。この段階で見ているのは、JS 側の配色ソースを読んでいる部分だけです。文字色のコントラスト不足、影の見え方、区切り線が背景に溶ける箇所。私自身の感覚では、ここで見つかる不具合が全体の8割ほどでした。

端末設定を切り替えて実機で見る(4か所のみ)

そのうえで端末をダークにし、コールドスタート、アラートを1つ、WebView を含む画面を1つ、Android ならシステムバーを含む画面を1つ。4か所だけを見ます。全画面を往復していた頃に比べて、確認そのものが数分で終わるようになりました。

切り替え直後の再描画を1回だけ確認

エミュレーションでも実機でも、切り替えた瞬間に再描画が走ります。ここで白画面が挟まる、状態が飛ぶ、といった症状が出るなら別の問題です。テーマ切り替えでプロセスが再起動する構成にしている場合は、テーマ切替の白画面を recreate() なしで直すで扱った内容が近いはずです。

手順が短くなると、確認の頻度が上がります

この記事で書いたことは、機能そのものの目新しさより、確認の摩擦が下がったという話に近いかもしれません。

ただ、7分かかる確認は「今回は前回から変えていないから飛ばそう」と思いますが、1分の確認は毎回走らせられます。頻度が上がったぶん、ダークモード起因の不具合が本番へ抜ける回数は確実に減りました。

次の一歩として、まずは診断画面を1つプロジェクトに置いてみてください。自分のバージョンでの境界が5分でわかりますし、そのあとの確認手順を自分の手で短くできます。

配色と同じく「実機でしか本当のところがわからない」領域として、VoiceOver と Dynamic Type の扱いをRork で VoiceOver と Dynamic Type を本番品質に整える実装メモにまとめてあります。あわせて手を入れると、設定まわりの確認をひとまとめにできます。

お読みいただきありがとうございました。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-09-04
EAS の secret は「アプリに入れない」設定ではありません — 接頭辞と可視性を別々に決める
EXPO_PUBLIC_ の接頭辞は成果物に入るかを決め、EAS の可視性は誰が読めるかを決めます。二つを重ねたときに OTA 更新でだけ値が消える筋道と、出荷前に自分の手で確かめる手順をまとめました。
開発ツール2026-09-01
expo-paste-input で画像を貼り付けられる入力欄を作り、消える file:// を documents へ逃がす
チャット入力に画像やGIFを貼り付けられるようにする expo-paste-input の実装手順です。onPaste が返す file:// は一時ファイルで、送信までの間に消えることがあります。保存先を移す実装と、実機で確かめる順番をまとめました。
開発ツール2026-08-31
画面ロックから3分後、expo-audio の環境音は静かに止まっていました
expo-audio の音が画面ロックで止まる原因を、ビルド設定・オーディオセッション・ロック画面連携の三層に分けて切り分けます。Android で約3分後に止まるのは、ドキュメントに書かれた止まり方です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます