WWDC Quick Look đź’“ By SwiftGGTeam
Deliver age-appropriate experiences in your app

Deliver age-appropriate experiences in your app

Watch original video

Highlight

Apple introduced the Declared Age Range API in iOS 26, letting apps fetch a user’s age range in a privacy-preserving way instead of an exact birthday. The idea is simple: the app submits the age cutoffs it cares about (say 13 and 16), and the system returns a range (say “13 to 15”) rather than a precise date of birth. This solves a long-standing dilemma for developers — they need to know a user’s age to gate content, but they do not want to take on the privacy risk of storing exact birthdays.


Core content

Developers who gate content by age have long faced an awkward problem. To decide “can this user use the social share feature,” the most accurate answer is the user’s birthday. But once you store that birthday, you face a chain of compliance audits — COPPA, GDPR-K, and more. So many apps just throw up a dialog that says “Confirm you are 13 or older” and let users tap through. That neither protects minors nor solves the compliance problem.

iOS 26 offers a middle layer. The Declared Age Range framework lets apps submit the age cutoffs they care about (up to 3, each range at least 2 years wide), and the system returns a range. Olivia is 14; the app asks about 13 and 16, and the system answers “13 to 15.” Emily is 9, and gets back “12 or younger.” Ann is 42, and gets back “16 or older.” The birthday stays inside the system. The app only gets the minimum information it needs to decide. Behind the age declaration is a parental flow: for child accounts, the age is confirmed by a guardian, and the three sharing modes (Always Share / Ask First / Never Share) are managed by the parent in Screen Time or Family settings. The response is cached for a year and synced across devices. The user is only re-prompted on the anniversary, so apps cannot pester users with repeated dialogs, and they cannot reverse-engineer the birthday by asking again and again.


Details

Adoption takes two steps. First, add the Declared Age Range capability under Signing & Capabilities in Xcode. Then make the request from SwiftUI through an environment value. Here is the official sample (08:03):

// Request an age range

import SwiftUI
import DeclaredAgeRange

struct LandmarkDetail: View {
    // ...
    @State var photoSharingEnabled = false
    @Environment(\.requestAgeRange) var requestAgeRange

    var body: some View {
        ScrollView {
            // ...
            Button("Share Photos") {}
                .disabled(!photoSharingEnabled)
        }
        .task {
            await requestAgeRangeHelper()
        }
    }

    func requestAgeRangeHelper() async {
        do {
            // TODO: Check user region
            let ageRangeResponse = try await requestAgeRange(ageGates: 16)
            switch ageRangeResponse {
            case let .sharing(range):
                 // Age range shared
                if let lowerBound = range.lowerBound, lowerBound >= 16 {
                    photoSharingEnabled = true
                }
                // guardianDeclared, selfDeclared
                print(range.ageRangeDeclaration)
            case .declinedSharing:
                // Declined to share
                print("Declined to share")
            }
        } catch AgeRangeService.Error.invalidRequest {
            print("Handle invalid request error")
        } catch AgeRangeService.Error.notAvailable {
            print("Handle not available error")
        } catch {
            print("Unhandled error: \(error)")
        }
    }
}

Key points:

  • import DeclaredAgeRange brings in the new framework. You must check the capability first or the build will not compile.
  • @Environment(\.requestAgeRange) gives you an async closure. The request lives on the environment so the system knows which window to show the prompt in — multi-window iPad and Mac both rely on this.
  • requestAgeRange(ageGates: 16) passes a single age cutoff, meaning the app only cares about the “16-year-old line.” If you need finer gating, pass more cutoffs (up to 3).
  • case let .sharing(range) unpacks range.lowerBound and range.upperBound. Both can be nil: a nil upper bound means “X and older,” and a nil lower bound means “X and younger.” That is why the code does if let lowerBound before comparing.
  • range.ageRangeDeclaration returns either guardianDeclared or selfDeclared. Children are always guardianDeclared, and teens in an iCloud Family are also guardianDeclared. Solo teens and adults are selfDeclared. Use this to decide whether you need stronger parental sign-off.
  • invalidRequest is a developer error (10:33) — usually a range narrower than two years. notAvailable is a device-configuration problem, such as the user not being signed into an Apple Account. Handle the two errors separately, not in one bucket.

If the upper bound of the returned range is below the local age of majority, the response also carries activeParentalControls, which the app can use to tighten the experience further (11:49):

// Request an age range

func requestAgeRangeHelper() async {
    do {
        // TODO: Check user region
        let ageRangeResponse = try await requestAgeRange(ageGates: 16)
        switch ageRangeResponse {
        case let .sharing(range):
            if range.activeParentalControls.contains(.communicationLimits) {
                print("Communication Limits enabled")
            }
            // ...
        case .declinedSharing:
            // Declined to share
            print("Declined to share")
        }
    } catch {
        // ...
    }
}

Key points:

  • activeParentalControls is a set. It currently includes flags like .communicationLimits that parents enable in Screen Time. When parents block contact with strangers, the app should turn off friend invites and stranger DM entry points.
  • The companion PermissionKit lets parents approve or deny specific contact requests inside third-party apps (12:08). It pairs with Declared Age Range: the age range sets the default switches, and PermissionKit handles per-case approval.
  • Caching strategy (11:21): the response is valid for a year, syncs across iPhone and Mac, and stays stable even if the app calls the API often. Users can manually reset the cache for a given app under Settings → Age Range for Apps, so the new range can take effect on the user’s birthday rather than waiting for the anniversary.

Key takeaways

  • What to do: Request the age range on demand at the feature entry point, not all at once at app launch.

    • Why it pays off: The API caches for a year, so the cost of asking is low. On-demand requests make it clear to the user “why am I being asked right now,” which lifts grant rates.
    • How to start: List the features in your app that depend on age (social sharing, chat, ads, in-app purchase), and attach .task { await requestAgeRangeHelper() } to each view. Ask for only the minimum age cutoffs that feature needs.
  • What to do: Treat “declined to share” as a degraded experience, not a blanket ban.

    • Why it pays off: A large share of users who pick Never Share are privacy-conscious adults. Treating them all as minors loses normal users.
    • How to start: In the .declinedSharing branch, keep browsing and lookup features open, and only hide entry points that need an age check. Use copy that explains “turn on age sharing to unlock the share feature.”
  • What to do: Use ageRangeDeclaration to tell guardianDeclared from selfDeclared, and decide how strict the secondary check should be.

    • Why it pays off: A child account’s range always comes from a guardian and is more trustworthy. Solo teens self-declare, so high-risk features (in-app purchase, going live) deserve an extra reminder.
    • How to start: Read range.ageRangeDeclaration in the .sharing(range) branch. For borderline selfDeclared users, add a parent email confirmation step or a delayed unlock policy.
  • What to do: When the response trips activeParentalControls.communicationLimits, hide stranger-contact entry points automatically.

    • Why it pays off: Once a parent turns on Communication Limits, leaving stranger entry points exposed is itself a compliance risk.
    • How to start: Keep a canMessageStrangers flag in the view model. Combine it with PermissionKit’s approval flow, and surface “denied” and “not yet approved” as two distinct states.

Comments

GitHub Issues · utterances