RORK LABEN
IOS27 — iOS 27 と iPadOS 27 は9月14日に配信されます。ノーコードや AI で組み立てたアプリにとって、新しい OS が降りてくる週は足場がいちばん揺れますDUO — Apple 初の折りたたみ iPhone Duo が 1,999 ドルから。新しい画面形状は、生成されたレイアウトが最初に壊れる場所でもありますSPLIT — 標準 Rork は Expo 経由の React Native、Rork Max はネイティブ Swift の生成です。折りたたみへの追随の速さも壊れ方も、同じにはならないはずですSUBMIT — 提出の自動化は、Apple 側の仕様変更をそのまま受けます。便利さを語る記事ほど、この揺れに触れないと不誠実になってしまいますRATING — 9月から年齢レーティング質問票への回答が必須になりました。ノーコードで作ったアプリも例外ではありませんSILICON — iPhone 18 Pro の A20 Pro は TSMC の 2nm ノードで作られる初の大量生産スマートフォン向けプロセッサだと報じられていますIOS27 — iOS 27 と iPadOS 27 は9月14日に配信されます。ノーコードや AI で組み立てたアプリにとって、新しい OS が降りてくる週は足場がいちばん揺れますDUO — Apple 初の折りたたみ iPhone Duo が 1,999 ドルから。新しい画面形状は、生成されたレイアウトが最初に壊れる場所でもありますSPLIT — 標準 Rork は Expo 経由の React Native、Rork Max はネイティブ Swift の生成です。折りたたみへの追随の速さも壊れ方も、同じにはならないはずですSUBMIT — 提出の自動化は、Apple 側の仕様変更をそのまま受けます。便利さを語る記事ほど、この揺れに触れないと不誠実になってしまいますRATING — 9月から年齢レーティング質問票への回答が必須になりました。ノーコードで作ったアプリも例外ではありませんSILICON — iPhone 18 Pro の A20 Pro は TSMC の 2nm ノードで作られる初の大量生産スマートフォン向けプロセッサだと報じられています
記事一覧/開発ツール
開発ツール/2026-09-10中級

画面を暗くする機能を足したら、アプリを閉じたあとも iPhone が暗いままでした

expo-brightness の setBrightnessAsync は、Android では現在のアクティビティにだけ効き、iOS では端末の明るさそのものを変えます。戻す責任が片側にしかない理由と、AppState で後始末をするフックの実装をまとめました。

expo-brightnessReact Native237Expo205iOS113Android49

寝る前に使っていただくことを前提にした環境音のアプリに、「暗くする」というボタンを足した夜のことです。手元の Android 端末で何度も試して、暗くなること、ホームに戻せば元に戻ることを確かめ、その日は満足して寝ました。

数日後、iPhone をお使いの方から連絡をいただきました。アプリを閉じたのに画面が暗いまま戻らない、設定を開いてスライダーを自分で上げた、という内容です。

こちらは同じ一行しか書いていません。それでも、その一行が触っていたものは二つの OS で別のものでした。

同じ setBrightnessAsync が、二つの端末で別のものを触っています

expo-brightness のドキュメントには、この違いがはっきり書かれています。読み落としていたのは私のほうでした。

観点iOSAndroid
setBrightnessAsync が変えるもの画面の明るさそのもの現在のアクティビティに対する上書き値
アプリを離れたときそのまま残りますシステムの値に戻ります
元に戻るきっかけ端末がロックされたときアプリがフォアグラウンドを離れたとき
必要な権限不要アプリ内の明るさだけなら不要

Android では、設定した値は「そのアクティビティがフォアグラウンドにある間だけシステムの値を上書きする」という形で効きます。だからホームボタンを押した瞬間に、こちらが何もしなくても明るさは戻ります。私が手元で確認して安心してしまったのは、この自動的な後始末に助けられていただけでした。

iOS には、アプリ単位の明るさという層がありません。書き込んだ値は端末の明るさそのものになり、端末がロックされるまで残ります。アプリを終了しても、ロックされるまでは暗いままなのです。

つまり後始末の必要は、片側にしかありません。そしてその片側を、私は書いていませんでした。

暗くしたのがアプリなら、明るさを返すのもアプリです。 この一文だけは、あれから機能を足すたびに先に思い出すようにしております。

元の明るさは、暗くする前に一度だけ控えます

戻すためには、戻す先の値が要ります。getBrightnessAsync() で読めるのですが、ここに小さな落とし穴がありました。

暗くしたあとにもう一度読むと、返ってくるのは自分が設定した値です。復元用の変数を毎回上書きする書き方にしていると、二度目の呼び出しで「暗い値」が控えとして保存され、戻したつもりが戻らなくなります。

控えるのは、最初の一度だけにします。

import { useRef } from 'react';
import * as Brightness from 'expo-brightness';
 
const originalRef = useRef<number | null>(null);
 
async function rememberOnce() {
  // すでに控えてあるなら読み直さない(読み直すと自分が下げた値を掴む)
  if (originalRef.current !== null) return;
  originalRef.current = await Brightness.getBrightnessAsync();
}

isAvailableAsync() も併せて呼んでおくと、対応していない環境で機能ごと隠せます。ボタンを出したのに何も起きない、という状態がいちばん不親切だからです。

戻す場所は、画面のアンマウントではなくアプリの状態です

最初のうち、私は復元処理を useEffect のクリーンアップだけに置いていました。画面を閉じれば戻るのだから、それで足りると考えたのです。結果は芳しくありませんでした。

読者の方がアプリを離れる動きは、画面を閉じることだけではありません。ホームに戻る、タスクスイッチャーから終了する、通知から別のアプリへ移る——このときクリーンアップは走らないことがあります。

そこで、明るさの上げ下げを AppState に紐づけました。

import { useCallback, useEffect, useRef } from 'react';
import { AppState, Platform } from 'react-native';
import * as Brightness from 'expo-brightness';
 
export function useDimScreen(target: number) {
  const originalRef = useRef<number | null>(null);
  const dimmingRef = useRef(false);
 
  const applyDim = useCallback(async () => {
    if (originalRef.current === null) {
      originalRef.current = await Brightness.getBrightnessAsync();
    }
    await Brightness.setBrightnessAsync(target);
  }, [target]);
 
  const restore = useCallback(async () => {
    const original = originalRef.current;
    if (original === null) return;
    await Brightness.setBrightnessAsync(original);
  }, []);
 
  const start = useCallback(async () => {
    if (!(await Brightness.isAvailableAsync())) return;
    dimmingRef.current = true;
    await applyDim();
  }, [applyDim]);
 
  const stop = useCallback(async () => {
    dimmingRef.current = false;
    await restore();
    originalRef.current = null;
  }, [restore]);
 
  useEffect(() => {
    const sub = AppState.addEventListener('change', next => {
      if (!dimmingRef.current) return;
      if (next === 'active') {
        applyDim();
      } else {
        // background と inactive の両方で返す。iOS は自動では戻りません
        restore();
      }
    });
    return () => {
      sub.remove();
      if (dimmingRef.current) restore();
    };
  }, [applyDim, restore]);
 
  return { start, stop };
}

inactive も拾っているのは、iOS で通知センターを引き下ろしたときやコントロールセンターを開いたときに、background まで進まず inactive で止まる場合があるからです。ここを取りこぼすと、少し戻したつもりが戻らない状態が残ります。

Android ではこの復元は空振りに近い動きになりますが、害はありません。分岐を増やして条件を二重に持つより、同じ経路で書いておくほうが後から読み返しやすいと感じております。

Android の system 系には手を出さない、と決めました

expo-brightness には、名前に System が入る関数の一群があります。ここは最初に迷ったところでした。

関数対応備考
getSystemBrightnessAsyncAndroid のみ端末全体の明るさを読みます
setSystemBrightnessAsyncAndroid のみ・実験的権限が必要で、明るさモードを MANUAL に変えます
restoreSystemBrightnessAsyncAndroid のみアクティビティの上書きをやめます
setSystemBrightnessModeAsyncAndroid のみ自動調整の入り切りです
addBrightnessListeneriOS のみAndroid と web では発火しません

最初のうち、私は「Android でも iOS と同じ深さまで暗くしたい」と考えて、system 側に手を伸ばしかけました。けれどもこちらを使うには WRITE_SETTINGS が要りますし、setSystemBrightnessAsync は端末全体の明るさを書き換えたうえで、自動調整を切ってしまいます。読者の方が普段どおり自動調整で使っていた設定を、環境音アプリが黙って手動に変えてしまうことになるのです。

権限を一つ増やせば、ストアの説明でもその必要性を書くことになります。アプリの中を暗くしたいだけなら、そこまでの代償を払う理由はありませんでした。

いまは、アプリの前面に出ているあいだにだけ効く範囲へ収める、という線引きにしています。iOS では自分で戻し、Android では OS に戻してもらいます。それぞれの流儀に合わせるほうが、結果として読者の端末を荒らしません。

暗くする機能は、画面を消させない設定と対で考えます

暗くしたい場面は、たいてい「眺めたまま眠りたい」場面です。ところが暗くしただけでは、しばらくすると OS が自動で画面を消します。せっかくの表示が数十秒で消えてしまう、という報告はここから来ます。

そのため私は、暗くするのと同時に自動消灯を止め、フェードが終わったところで解除する形にしました。点けっぱなしは電池を確実に削りますので、解除の条件を先に決めてから入れるほうが安心です。

もう一つ、iOS では addBrightnessListener が使えます。読者の方がコントロールセンターで自分から明るさを変えた場合、こちらが控えていた値はもう古い値です。リスナーで気づいたときに控えを取り直しておくと、戻したときの違和感が減ります。同じ発想の後始末は音のほうにもあり、画面ロックから3分後、expo-audio の環境音は静かに止まっていました に書き残しました。画面が消えてからの挙動を扱うアプリは、どの機能でも「離れたあと」を先に決める必要があるのだと思います。

今夜のうちに確かめられること

実機が二台あれば、十分ほどで確認できます。iPhone で暗くするボタンを押し、ホームに戻り、アプリを終了してから設定アプリの明るさを見てください。スライダーが下がったままなら、復元が走っていません。Android では、ホームに戻った時点で明るさが戻るはずです。

iOS 27 と iPadOS 27 は9月14日に配信されます。明るさのように OS 側の既定へ直接触る箇所は、更新の前に一度動かしておくと落ち着いて迎えられます。手順は iOS 27 が正式版になる前に、予備の iPhone で公開中の Rork アプリを触っておく にまとめてあります。

まずは、いま暗くしている画面から離れたときに何が起きるかを、一度だけ手で試してみていただければと思います。私もそこで気づきました。お読みいただきありがとうございました。

シェア

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

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

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

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

関連記事

開発ツール2026-04-02
Rork アプリにネイティブ共有機能を追加する — Share Sheet 実装ガイド
RorkアプリにネイティブのShare Sheetを実装する入門です。expo-sharingとreact-native-shareの選び方、テキスト・URL・画像それぞれのシェア実装、よくあるエラーの対処、シェアイベントのトラッキングまで実際のコードで丁寧に解説します。
開発ツール2026-08-31
画面ロックから3分後、expo-audio の環境音は静かに止まっていました
expo-audio の音が画面ロックで止まる原因を、ビルド設定・オーディオセッション・ロック画面連携の三層に分けて切り分けます。Android で約3分後に止まるのは、ドキュメントに書かれた止まり方です。
開発ツール2026-07-27
最低サポート iOS をいつ上げるか — 利用率ではなく、コードに残った分岐の数で決める
最低サポート OS の引き上げを利用率で判断すると、毎年同じ結論になって先送りが続きます。OS 分岐に注記を付けて棚卸しし、iOS と Android の引き上げ先ごとに消せる分岐を数えるスクリプトと、引き上げ後に見る指標をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます