Declared Age Range and PermissionKit: Privacy-Preserving Age Verification
Your app needs to know whether a user is a child, a teen, or an adult — and in an increasing number of jurisdictions,
the law requires it. But collecting a birthdate is a privacy liability, parental consent flows are painful, and
rolling your own age-gate is both fragile and legally questionable. iOS 26 introduces two frameworks that solve this at
the system level: DeclaredAgeRange and PermissionKit.
In this post you will learn how DeclaredAgeRange returns a parent-verified age category without ever exposing a
birthdate, how PermissionKit lets a child’s device route permission requests to a parent, and how to integrate both
into a production app. We will not cover Family Sharing setup or Screen Time configuration — those are system-level
concerns outside your app’s control.
Contents
- The Problem
- How DeclaredAgeRange Works
- Querying the Age Range
- PermissionKit: Requesting Parental Approval
- Advanced Usage and Edge Cases
- When to Use (and When Not To)
- Summary
The Problem
Consider a streaming app that must gate mature content. A naive approach might prompt the user for their birth year and store it locally:
struct PixarStreamingProfile {
let username: String
var birthYear: Int // Privacy liability
var isMinor: Bool {
let currentYear = Calendar.current.component(.year, from: .now)
return currentYear - birthYear < 18
}
}
let profile = PixarStreamingProfile(username: "andy_davis", birthYear: 2015)
if profile.isMinor {
// Hide "Alien" and other PG-13+ content
print("Restricted content library for \(profile.username)")
}
This compiles, but it is riddled with problems. The user can lie about their age. You are now storing age data you may be legally required to protect under COPPA, the EU Digital Services Act, or the UK Age Appropriate Design Code. There is no parental verification. And if your compliance audit asks “how do you verify age?”, the answer is “we trust a text field” — which is not an answer regulators accept.
Apple’s DeclaredAgeRange framework eliminates all of these issues by pushing age verification into the operating
system where it belongs.
How DeclaredAgeRange Works
DeclaredAgeRange is a lightweight framework introduced in iOS 26 that returns the age category of the current device
user as declared and verified by a parent or guardian through Family Sharing. The framework never exposes a specific age
or birthdate — it returns a category: child, teen, or adult.
The verification chain works like this:
- A parent sets up a child’s Apple Account through Family Sharing and confirms the child’s age.
- The system stores a verified age range on-device, protected by the Secure Enclave.
- Your app queries
DeclaredAgeRangeand receives one of a small set of age categories — no network call required.
This design follows Apple’s established pattern of keeping sensitive data on-device and exposing only the minimum information an app needs. The same philosophy powers App Tracking Transparency and SKAdNetwork.
Apple Docs:
DeclaredAgeRange— DeclaredAgeRange Framework
Querying the Age Range
The API surface is deliberately minimal. You request the current user’s age range through a single asynchronous call:
import DeclaredAgeRange
struct ContentGatingService {
enum ContentTier {
case allAges // "Toy Story", "Finding Nemo"
case teen // "Turning Red", "Brave"
case mature // "Alien" (yes, Disney owns it now)
}
func allowedContentTier() async -> ContentTier {
let ageRange = await DeclaredAgeRange.current
switch ageRange {
case .child:
return .allAges
case .teen:
return .teen
case .adult:
return .mature
default:
// Unknown or not declared -- default to the most
// restrictive tier for compliance safety
return .allAges
}
}
}
A few things worth noting in this code:
- The call is
asyncbut does not hit the network. It reads from the on-device secure store. Latency is negligible. - The
defaultcase is critical. If the device user has no Family Sharing configuration or the age range is undeclared, you must decide on a fallback. Defaulting to the most restrictive tier is the legally safer choice. - There is no authorization prompt. Unlike HealthKit or location services, the system does not show a permission dialog. The age range is considered non-sensitive metadata about the account’s parental configuration, not personal health or location data.
Reacting to Age Range Changes
A child’s age category can change — they have a birthday, or a parent updates Family Sharing settings. You should not cache the age range at launch and assume it never changes. Instead, observe updates:
import DeclaredAgeRange
import Combine
final class AgeRangeMonitor: ObservableObject {
@Published private(set) var currentRange: DeclaredAgeRange.Category?
private var task: Task<Void, Never>?
func startMonitoring() {
task = Task {
for await range in DeclaredAgeRange.updates {
await MainActor.run {
self.currentRange = range
}
}
}
}
func stopMonitoring() {
task?.cancel()
task = nil
}
}
The DeclaredAgeRange.updates stream emits a new value whenever the on-device age category changes. Wrapping this in an
ObservableObject lets you drive SwiftUI view updates directly.
Tip: Pair this monitor with your app lifecycle handling. Start monitoring in
scenePhase == .activeand cancel in.backgroundto avoid unnecessary work.
PermissionKit: Requesting Parental Approval
Some features cannot be unlocked by age range alone. Suppose your Pixar streaming app wants to let a child share a movie
review publicly. Even if the child’s age range is .teen, your compliance team may require explicit parental consent
for public-facing content.
PermissionKit handles this. It presents a system-mediated request that routes from the child’s device to the parent’s
device through Family Sharing. The parent approves or denies, and your app receives the result — all without you
building any communication infrastructure.
import PermissionKit
struct ReviewPublisher {
let movieTitle: String // e.g., "Inside Out 2"
func requestPublishPermission() async throws -> Bool {
let request = PermissionRequest(
type: .publicContent,
reason: "\(movieTitle) review will be visible to other users"
)
let result = try await PermissionKit.requestAuthorization(request)
switch result {
case .approved:
return true
case .denied:
return false
case .pending:
// Parent hasn't responded yet -- treat as denied for now
return false
}
}
}
When requestAuthorization is called on a child’s device, the system sends a notification to the parent’s device. The
parent sees a system alert with the reason string you provided and can approve or deny. This flow is entirely managed
by iOS — your app never communicates directly with the parent’s device.
Apple Docs:
PermissionKit— PermissionKit Framework
Combining DeclaredAgeRange and PermissionKit
In practice, you will often use both frameworks together. Query the age range first to determine if you need parental permission, then use PermissionKit only when necessary:
import DeclaredAgeRange
import PermissionKit
struct MovieReviewGate {
func canPublishReview(for movieTitle: String) async throws -> Bool {
let ageRange = await DeclaredAgeRange.current
switch ageRange {
case .adult:
// Adults don't need parental approval
return true
case .teen:
// Teens need explicit parental consent for public content
let publisher = ReviewPublisher(movieTitle: movieTitle)
return try await publisher.requestPublishPermission()
case .child:
// Children cannot publish public reviews regardless
return false
default:
// Undeclared age -- block public content by default
return false
}
}
}
This layered approach keeps the UX clean. Adults never see a permission prompt. Children are silently restricted. Only teens — the category where parental judgment genuinely varies — trigger the parent approval flow.
Advanced Usage and Edge Cases
Devices Without Family Sharing
Not every device is configured with Family Sharing. When DeclaredAgeRange cannot determine an age category, it returns
an undeclared or unknown value. Your app must handle this gracefully:
func resolveAgeRange() async -> ContentGatingService.ContentTier {
let ageRange = await DeclaredAgeRange.current
guard ageRange != .notDeclared else {
// No Family Sharing configuration detected.
// Present an in-app age gate as a fallback,
// but understand it carries less legal weight.
return .allAges
}
// Proceed with system-verified age range
return ContentGatingService().allowedContentTier(for: ageRange)
}
Warning: Falling back to a self-serve age gate when
DeclaredAgeRangereturns.notDeclaredis a reasonable UX choice, but consult your legal team about whether it satisfies compliance in your target jurisdictions. Some regulations (notably the UK Age Appropriate Design Code) may require stronger verification.
PermissionKit Timeouts and Pending State
A parent might not respond immediately — or at all. PermissionKit returns .pending when the request has been sent
but not yet resolved. Design your UI to handle this state explicitly:
struct ReviewSubmissionView: View {
@State private var permissionState: PermissionState = .unknown
enum PermissionState {
case unknown, requesting, approved, denied, pending
}
var body: some View {
Group {
switch permissionState {
case .unknown:
Button("Publish Review") { Task { await requestPermission() } }
case .requesting:
ProgressView("Asking your parent...")
case .approved:
Text("Review published!")
case .denied:
Text("Your parent didn't approve this time.")
case .pending:
Text("Waiting for your parent to respond.")
}
}
}
private func requestPermission() async {
permissionState = .requesting
let publisher = ReviewPublisher(movieTitle: "Inside Out 2")
do {
let approved = try await publisher.requestPublishPermission()
permissionState = approved ? .approved : .denied
} catch {
permissionState = .denied
}
}
}
Thread Safety
Both DeclaredAgeRange.current and PermissionKit.requestAuthorization are async APIs designed for Swift concurrency.
They are safe to call from any actor context. However, if you store the result in a shared mutable property, ensure you
update it on the appropriate actor — typically @MainActor for anything driving UI, as shown in the AgeRangeMonitor
example above.
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| App serves content to users under 18 | Use DeclaredAgeRange to gate content tiers. This is the legally strongest approach on iOS. |
| Child needs to perform a sensitive action (public posting, in-app purchases beyond a threshold) | Use PermissionKit to route an explicit approval request to the parent. |
| App is enterprise-only or exclusively targets adults | Skip both frameworks. They add no value when your user base has no minors. |
| You need a precise age (e.g., 14 vs. 15) for fine-grained rules | DeclaredAgeRange does not provide this. You will need a server-side age verification service in addition to the system age range. |
| You want to replace your existing third-party age verification SDK | DeclaredAgeRange can replace it on iOS 26+, but you will still need the third-party solution for older OS versions and non-Apple platforms. |
Note:
DeclaredAgeRangeandPermissionKitrequire iOS 26 or later. Use@available(iOS 26, *)annotations and provide fallback paths for earlier deployment targets.
Summary
DeclaredAgeRangereturns a parent-verified age category (child, teen, adult) without exposing a birthdate or requiring a network call.PermissionKitroutes permission requests from a child’s device to a parent’s device through Family Sharing, with no custom infrastructure needed.- Always handle the undeclared/unknown state — not every device has Family Sharing configured.
- Default to the most restrictive content tier when age cannot be determined. This is both the safest UX and the legally defensible choice.
- Use
@available(iOS 26, *)and provide fallback age-gating for older deployment targets.
These frameworks make compliance-grade age verification a system-level concern rather than an app-level burden. For the authentication side of your privacy story, see Passkeys and AuthenticationServices to understand how iOS 26 handles credential creation and management.