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

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:

  1. A parent sets up a child’s Apple Account through Family Sharing and confirms the child’s age.
  2. The system stores a verified age range on-device, protected by the Secure Enclave.
  3. Your app queries DeclaredAgeRange and 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 async but does not hit the network. It reads from the on-device secure store. Latency is negligible.
  • The default case 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 == .active and cancel in .background to 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 DeclaredAgeRange returns .notDeclared is 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)

ScenarioRecommendation
App serves content to users under 18Use 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 adultsSkip 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 rulesDeclaredAgeRange 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 SDKDeclaredAgeRange can replace it on iOS 26+, but you will still need the third-party solution for older OS versions and non-Apple platforms.

Note: DeclaredAgeRange and PermissionKit require iOS 26 or later. Use @available(iOS 26, *) annotations and provide fallback paths for earlier deployment targets.

Summary

  • DeclaredAgeRange returns a parent-verified age category (child, teen, adult) without exposing a birthdate or requiring a network call.
  • PermissionKit routes 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.