枕元で充電している iPhone を横向きに置くと、画面に大きな時計やウィジェットが並びます。これが iOS 17 から入った StandBy モードです。先日、自分が個人開発で運営しているアファメーション系アプリ「Law of Attraction Everyday」のウィジェットを Rork Max で作り直していたとき、ホーム画面では問題なく見えていたものが、StandBy に置いた途端に文字が切れ、夜間の暗い赤色表示では何が書いてあるか読めない状態になりました。
StandBy は単に「ウィジェットを大きく出すモード」ではありません。常時表示(Always-On Display)での調光、深夜のナイトモード、横向き専用のレイアウトという三つの異なる環境を同時に相手にすることになります。Rork Max は WidgetKit の足場までは自然言語から作ってくれますが、この三つの環境差を埋める部分は、生成されたコードを読んで自分で詰める必要がありました。その過程で分かったことを、実装手順としてまとめます。
StandBy が発動する条件と、何が変わるのか
まず前提として、StandBy は次の三条件がすべて揃ったときだけ発動します。
iPhone が充電中であること(MagSafe・Lightning・USB-C いずれでも可)
横向きに固定されていること
ロック状態であること
この条件は開発時に見落としやすく、シミュレータでは再現が安定しません。私自身、最初はシミュレータで確認しようとして時間を溶かしました。StandBy の挙動確認は実機を横向きで充電する以外に確実な方法がないと考えておいたほうが安全です。
StandBy のウィジェットは、ホーム画面の systemSmall ファミリーをそのまま流用します。つまり新しいウィジェットファミリーを追加するのではなく、既存の小ウィジェットが StandBy のスタックに並ぶ形です。ここで効いてくるのが、環境値による表示の出し分けです。
環境 判定に使う環境値 求められる対応
通常のホーム画面 既定 フルカラー・通常の情報量
StandBy 昼間 widgetRenderingMode = fullColor 余白を広げ、文字を大きく
StandBy ナイトモード isLuminanceReduced = true 赤系の単色・最小限の要素・発光面積を減らす
常時表示(14 Pro 以降) isLuminanceReduced = true 1Hz 更新前提・アニメーション抑制
ナイトモードと常時表示はどちらも isLuminanceReduced で拾えますが、意味合いは少し違います。前者は「暗い部屋で眩しくしない」ため、後者は「焼き付きと電力を抑える」ためです。実装上はどちらも「発光面積を減らし、動きを止める」方向に倒せば両立できます。
手順1: コンテナ背景を必ず宣言する
iOS 17 以降、ウィジェットは containerBackground(for: .widget) で背景を宣言しないと、StandBy やロック画面で背景が消えて文字だけが浮く、あるいはレイアウトが詰まる現象が起きます。Rork Max が生成したコードでは、この宣言が ZStack の手書き背景になっていることがあり、そのままだと StandBy で破綻しました。
struct AffirmationWidgetView : View {
var entry: AffirmationEntry
var body: some View {
VStack ( alignment : .leading, spacing : 6 ) {
Text (entry.affirmation)
. font (.headline)
. minimumScaleFactor ( 0.6 ) // StandBy で切れないよう縮小を許可
. lineLimit ( 3 )
Spacer ( minLength : 0 )
Text (entry.date, style : .time)
. font (.caption2)
. foregroundStyle (.secondary)
}
// ❌ 手書き背景だけだと StandBy で背景が剥がれる
// .background(Color.indigo)
// ✅ システムにコンテナ背景として認識させる
. containerBackground ( for : .widget) {
Color.indigo
}
}
}
minimumScaleFactor を入れているのは、StandBy では同じ systemSmall でも実効的な表示領域が広がり、逆にフォントが相対的に小さく見えるためです。固定フォントサイズのままだと、長いアファメーション文が途中で切れます。ここはホーム画面では問題が出ず、StandBy で初めて露見する典型的な箇所でした。
手順2: 調光(ナイトモード・常時表示)に応える
isLuminanceReduced が true のときは、白や明るい色の面積をできるだけ削ります。私の場合、夜間に枕元へ置く使い方を想定しているアプリだったので、ここの作り込みが体験を大きく左右しました。
struct AffirmationWidgetView : View {
@Environment (\.isLuminanceReduced) private var dimmed
var entry: AffirmationEntry
var body: some View {
VStack ( alignment : .leading, spacing : dimmed ? 4 : 6 ) {
Text (entry.affirmation)
. font (dimmed ? .subheadline : .headline)
. foregroundStyle (dimmed ? .red. opacity ( 0.85 ) : .white)
. lineLimit (dimmed ? 2 : 3 )
. minimumScaleFactor ( 0.6 )
if ! dimmed {
// 調光時は時刻などの補助情報を消し、発光面積を減らす
Text (entry.date, style : .time)
. font (.caption2)
. foregroundStyle (.secondary)
}
}
. containerBackground ( for : .widget) {
dimmed ? Color.black : Color.indigo
}
}
}
ポイントは、調光時に「色を暗くする」だけでなく「要素そのものを減らす」ことです。発光しているピクセルの総量が体感の眩しさと電力に直結するため、補助テキストや装飾は思い切って消したほうが、夜間の使用感が良くなります。私は最初、色だけ赤くして要素は残していましたが、実機で枕元に置くと想像以上に明るく、結局この削る判断に落ち着きました。
手順3: 常時表示の更新頻度を前提に組む
常時表示の iPhone では、ウィジェットの更新が概ね 1Hz に制限され、WidgetKit のタイムライン更新予算(1日あたりおよそ40〜70回程度)も別枠で消費し続けます。秒単位で動くカウントダウンのような表現は、常時表示では滑らかには動きません。
ここは「動かす前提」を捨て、Text(date, style: .timer) のようにシステムが面倒を見てくれる表示に寄せるのが現実的です。タイムライン側で毎分エントリを作るような設計にすると、予算を一気に使い切って深夜に更新が止まるエラーに近い挙動を招きます。本番で配信する前に、この更新予算の罠は必ず回避しておくことを推奨します。更新予算の設計は、StandBy 専用ではなくウィジェット全般の土台になる話なので、WidgetKit のタイムライン更新予算の設計 も併せて確認しておくと、StandBy 対応が一段安定します。
手順4: 操作可能ウィジェットは StandBy でも効く
iOS 17 以降、App Intents を使った操作可能ウィジェット(ボタンやトグル)は StandBy 上でも動作します。アファメーションを「次の一文へ送る」ボタンを置くと、枕元から手を伸ばすだけで切り替えられて便利でした。
struct NextAffirmationIntent : AppIntent {
static var title: LocalizedStringResource = "次のアファメーション"
func perform () async throws -> some IntentResult {
AffirmationStore.shared. advance ()
return . result ()
}
}
// ウィジェット側
Button ( intent : NextAffirmationIntent ()) {
Image ( systemName : "arrow.right.circle" )
}
. buttonStyle (.plain)
ただし調光時はタップ領域が見えにくくなるため、isLuminanceReduced が true のときはボタンを非表示にするか、輪郭だけ残す判断が要ります。App Intents 自体の設計はApp Intents と Siri ショートカットの統合 で扱っているので、操作の入口を Siri と共有したい場合はそちらが参考になります。
Rork Max が担う範囲と、自分で詰める範囲
ここまでの実装を通して感じた責任分界を、率直に整理します。Rork Max は、WidgetKit の Provider・Entry・基本ビューという足場を、自然言語の指示からかなり正確に組んでくれます。systemSmall のレイアウトや App Intents の雛形までは、生成された時点で動く状態でした。
一方で、StandBy の三環境(昼・ナイト・常時表示)を出し分ける isLuminanceReduced 分岐、containerBackground への置き換え、更新予算を踏まえたタイムライン設計は、生成コードには入っていませんでした。これは Rork Max の弱点というより、StandBy が「実機を横向きで充電して初めて見える」性質を持つため、生成時のプレビューだけでは検出しようがない領域だからだと考えています。
私の運用ルールとしては、ウィジェットの骨格は Rork Max に任せ、StandBy 固有の調整は必ず実機で一周見てから手で入れる、という分担に落ち着きました。生成 AI を使う場合でも、こうした「プレビューに映らない環境差」は人が見て詰めるしかない、と割り切るのが結局いちばん速いと感じています。
領域 Rork Max 生成 手で詰める
Provider / Entry / 基本ビュー ◎ ほぼそのまま動く 軽微
containerBackground 宣言 △ 手書き背景になりがち 必須
isLuminanceReduced 分岐 ✕ 生成されない 必須
更新予算を踏まえたタイムライン △ 毎分更新を作りがち 必須
操作可能ウィジェット(App Intents) ◎ 雛形は正確 調光時の表示のみ
動作確認のときに見るべき三点
実機で StandBy を確認するとき、私が必ず見ているのは次の三点です。まず、横向き充電・ロック状態でウィジェットが意図したレイアウトで出るか。次に、部屋を暗くして数十秒待ち、ナイトモードに切り替わったときに文字が読めるか。最後に、14 Pro 以降であれば常時表示のまま放置し、更新が深夜に止まらないかを翌朝確認します。
特に三点目は、タイムライン予算を使い切る設計だと「夜中の2時で時刻が固まっている」という形で表面化します。これは App Store のレビューでも指摘されやすく、ウィジェットの基礎データの鮮度問題とも絡むので、ウィジェットの App Group 経由データが古くなる設計 で扱っている鮮度管理と合わせて見ておくと安心です。
まずは手元のウィジェットを一つ、containerBackground の宣言と isLuminanceReduced 分岐だけ入れて、実機を横向きで充電してみてください。ホーム画面では気づけなかった崩れが、その場で見えてくるはずです。