ある朝、ストア審査前に気づいたこと
壁紙アプリの小さな更新を出そうとして、App Store Connect の年齢関連の設定を見直していたときのことです。これまで「対象年齢」を申告すれば済んでいた欄に、地域によっては実際の年齢確認が求められる、という新しい前提が入り込んでいました。
最初に頭をよぎったのは、生年月日の入力欄を足すという昔ながらの発想でした。けれど、個人開発の小さなアプリで生年月日を預かるのは、保存も削除依頼への対応も含めて重すぎます。集めた瞬間に、それは守らなければならない個人情報になります。
iOS 26 で追加された Declared Age Range API は、ここに別の道を示してくれます。アプリは「誕生日」ではなく「年齢層」だけをシステムに尋ね、OS が iCloud アカウントの情報をもとに必要最小限の答えを返す。アプリ側に生年月日は一切渡りません。この API を Rork で作ったアプリに組み込む手順を、Rork Max(ネイティブ Swift)と標準 Rork(Expo)の両方の経路で、順を追って見ていきます。
この API が解こうとしている問題
従来の年齢確認には、構造的なジレンマがありました。正確さを求めると生年月日や身分証を集めることになり、プライバシーと運用コストが膨らみます。逆に「あなたは13歳以上ですか?」というチェックボックスだけでは、確認したことにはなりません。
Declared Age Range API は、この板挟みを OS 側に肩代わりさせます。アプリが宣言するのは「自分のアプリにとって意味のある年齢の境界(age gate)」だけです。たとえば 13・16・18 という境界を渡すと、システムは利用者に確認を取り、その人がどの区間に収まるかだけを返します。アプリは「16歳以上の区間に入っている」ことは分かっても、正確な年齢や誕生日は受け取りません。
私自身、この設計の割り切り方には納得感があります。アプリが本当に必要としているのは「この機能を出してよい相手か」という判断材料であって、誕生日そのものではないからです。
必要な準備 — Capability とプラットフォーム条件
実装に入る前に、確認しておくべき前提が三つあります。
Capability の追加 : App ターゲットに com.apple.developer.declared-age-range の Capability を追加します。これがないと API 呼び出しは権限エラーで失敗します。Rork Max でネイティブ Swift を生成している場合は、生成された Xcode プロジェクトの entitlements にこのキーが含まれているかを確認してください。
対応プラットフォーム : この API は iOS 26 / iPadOS 26 / macOS 26 以降で利用できます。それ以前の OS では呼び出せないため、後述するフォールバックが必須になります。
適用範囲の見極め : 法的に年齢確認が必要になる代表例として、テキサス州では 2026年1月1日以降に作成された新規 Apple アカウントに対して確認義務が生じます。この義務は iOS / iPadOS アプリが対象で、全利用者・全地域に一律で年齢ゲートを課すものではありません。
ここを取り違えると、対象でない利用者にまで不要な摩擦を与えてしまいます。適用範囲を先に固めてから実装に入ることを強くお勧めします。
Rork Max(ネイティブ Swift)での実装
Rork Max が生成するのはネイティブ Swift なので、API を素直に呼べます。SwiftUI では、環境値 requestAgeRange としてアクションが渡ってきます。
次のコードは「16歳以上かどうか」で表示する画面を切り替える最小構成です。何を解決しているかというと、生年月日を一切保存せずに、機能の出し分けに必要な一点だけを得る、という処理です。
import SwiftUI
import DeclaredAgeRange
struct AgeGatedView : View {
@Environment (\.requestAgeRange) private var requestAgeRange
@State private var isAdultContentAllowed = false
@State private var didAsk = false
var body: some View {
Group {
if isAdultContentAllowed {
MatureFeatureView ()
} else {
StandardFeatureView ()
}
}
. task {
guard ! didAsk else { return }
didAsk = true
await checkAgeRange ()
}
}
private func checkAgeRange () async {
do {
// アプリにとって意味のある境界だけを宣言する
let response = try await requestAgeRange ( ageGates : 13 , 16 , 18 )
switch response {
case . sharing ( let range) :
// 下限が取れたときだけ厳密に判定する
if let lowerBound = range.lowerBound, lowerBound >= 16 {
isAdultContentAllowed = true
} else {
isAdultContentAllowed = false
}
case .declinedSharing :
// 共有を断られた場合は安全側に倒す
isAdultContentAllowed = false
@unknown default:
isAdultContentAllowed = false
}
} catch {
// 権限なし・OS 非対応などは安全側のデフォルトへ
isAdultContentAllowed = false
}
}
}
ここで大事なのは、lowerBound が nil になりうる点です。システムが境界を確定できないケースでは下限が返らないので、if let で取り出してから比較します。nil を「条件を満たさない」とみなして安全側に倒すのが基本方針です。
なぜ境界を 13・16・18 のように複数渡すのかというと、システムはこちらが宣言した境界に沿って区間を返すからです。アプリが 16 でしか分岐しないなら 16 だけでも動きますが、将来の機能追加を見越して、意味のある境界をまとめて渡しておくと判定の幅が広がります。
「誰が宣言したか」を扱う — 自己申告と保護者申告
応答に含まれる年齢層には、それが本人の自己申告か、ファミリー共有で保護者が宣言したものかという属性が付きます。子ども向けの体験を出し分けたいときは、この違いが効いてきます。
case . sharing ( let range) :
switch range.ageRangeDeclaration {
case .guardianDeclared :
// 保護者がファミリー設定で宣言した年齢層
applyChildSafeDefaults ()
case .selfDeclared :
// 本人が申告した年齢層
applyStandardExperience ()
@unknown default:
applyChildSafeDefaults ()
}
保護者申告であれば、未成年向けの安全なデフォルト(広告のパーソナライズ抑制、外部リンクの制限など)をより強めに当てる、という設計が自然です。個人的には、自己申告と保護者申告を一緒くたにせず、判断に迷う場面では保守的な側に寄せることを推奨します。後からの審査やトラブルへの備えとして効いてきます。
標準 Rork(Expo)から呼ぶ — ネイティブモジュールの境界
ここが、Rork を使う多くの方にとっての本題かもしれません。標準 Rork は React Native(Expo)基盤なので、JavaScript / TypeScript から requestAgeRange を直接呼ぶことはできません。この API は Swift 側にしか存在しないからです。
そのため、薄いネイティブモジュールを一枚かませて橋渡しします。Expo Modules API を使うと、Swift の関数を JS から await で呼べる形に包めます。
// modules/age-range/ios/AgeRangeModule.swift
import ExpoModulesCore
import DeclaredAgeRange
import SwiftUI
public class AgeRangeModule : Module {
public func definition () -> ModuleDefinition {
Name ( "AgeRange" )
// JS からは await AgeRange.checkLowerBound(16) のように呼ぶ
AsyncFunction ( "checkLowerBound" ) { ( gate : Int ) -> Bool in
guard #available ( iOS 26.0 , * ) else { return false }
let service = AgeRangeService ()
let response = try await service. requestAgeRange ( ageGates : 13 , 16 , 18 )
switch response {
case . sharing ( let range) :
if let lowerBound = range.lowerBound {
return lowerBound >= gate
}
return false
case .declinedSharing :
return false
@unknown default:
return false
}
}
}
}
JS 側はこれを呼ぶだけです。
import { requireNativeModule } from "expo-modules-core" ;
type AgeRangeModule = {
checkLowerBound ( gate : number ) : Promise < boolean >;
};
const AgeRange = requireNativeModule < AgeRangeModule >( "AgeRange" );
export async function isAtLeast ( gate : number ) : Promise < boolean > {
// iOS 以外・古い OS では呼べないので false(安全側)を返す
if (Platform. OS !== "ios" ) return false ;
try {
return await AgeRange. checkLowerBound (gate);
} catch {
return false ;
}
}
Expo でネイティブモジュールを足す手順そのものは、別記事のExpo の Dev Client とネイティブモジュール設定 で扱っているので、土台がまだの方はそちらを先にご覧ください。本稿では「年齢確認をネイティブ側に閉じ込める」という設計判断に集中します。
再生成で壊さないための配置
Rork や Rork Max で開発していると、画面のコードは AI による再生成で書き換わることがあります。ここに年齢確認のロジックを直接書き込むと、再生成のたびに消えたり改変されたりして、運用が不安定になります。
私が実践で落ち着いたのは、年齢確認を「再生成されない層」に隔離するやり方です。具体的には、AgeRangeModule.swift と JS 側の isAtLeast() ラッパーは手書きの独立ファイルとして固定し、生成される画面側からはこのラッパー関数を呼ぶだけにします。画面が再生成されても、呼び出し口(isAtLeast(16))という契約さえ保てば、判定の本体は無傷で残ります。
この「生成される領域」と「手で守る領域」の線引きは、年齢確認に限らず Rork 運用の肝になる考え方です。境界設計そのものについてはRork Max と Expo の責務境界の設計 も合わせて参考になると思います。
フォールバックと、クライアント判定の限界
実装で見落としがちな二点を、最後に補足します。
一つ目は OS バージョンのフォールバックです。iOS 26 未満では API そのものが存在しないため、#available で分岐し、呼べない環境では機能を「出さない/既定の安全な状態にする」方針を明確にしておきます。古い OS の利用者に対して、年齢確認を理由に体験を完全に止めてしまうと、対象でない人にまで摩擦を与えます。法的に確認が必要な範囲はどこまでかを先に決め、それ以外は通常体験を維持するのが現実的です。
二つ目は、クライアント判定を過信しないことです。Declared Age Range API はデバイス上の判断であり、サーバーが検証できる署名付きの証明ではありません。広告の出し分けや UI の切り替えといった、クライアントで完結する用途には向いていますが、課金や決済の最終的なゲートを年齢だけに依存させるような設計には向きません。サーバー側のロジックは、クライアントからの年齢層をあくまで「補助的なシグナル」として扱うのが安全です。
次の一歩
まずは自分のアプリで、年齢で出し分けたい機能が本当にあるかを一つ書き出してみてください。広告のパーソナライズなのか、UGC の表示なのか、特定カテゴリのコンテンツなのか。その一点が決まれば、宣言すべき age gate は自然に定まります。
そのうえで、isAtLeast(gate) という小さな関数を一つだけ手書きで用意し、再生成される画面からはそれを呼ぶ形にしておく。この最小の足場ができていれば、テキサス州のような地域別の要件が増えても、判定の入り口は一箇所に集約されたまま対応できます。
同じように個人開発で年齢確認に向き合っている方の、設計の出発点になれば幸いです。