BS
BleepingSwift
Published on
8 min read
Intermediate

> The New EU App Tracking Transparency Prompt in iOS 27.2

Share:

Apple announced changes to App Tracking Transparency in the European Union today, and the first beta of iOS 27.2 already has the API for them. The rules about when you need permission to track haven't moved at all. What changes is the prompt itself: EU users can see a full-page version of it with formatted text and an optional "Additional Information" button, and for the first time you're allowed to ask again after a year.

According to Apple's announcement, this is part of agreements with European competition authorities. In five countries the new version isn't optional, so even if you never touch the new API, some of your users will see a different prompt once they update to iOS 27.2. If you haven't worked with ATT before, start with my App Tracking Transparency in SwiftUI guide, since everything below builds on it.

Who Sees Which Prompt

Which prompt appears depends on where the person is and on the API you call. Apple checks two things for eligibility: the device has to be located in a specific EU country, and the person has to be signed in with an Apple Account whose country or region is set to one of them.

RegionrequestTrackingAuthorization()New expanded-interface API
France, Germany, Italy, Poland, RomaniaFull-page sheetFull-page sheet, whatever you pass for usingExpandedInterface
Rest of the EUStandard alertFull-page sheet if you pass true, standard alert otherwise
Everywhere elseStandard alertStandard alert, both parameters ignored

The first row surprises people. In those five countries the existing requestTrackingAuthorization(completionHandler:) method now presents the full-page sheet too, because legal requirements rule out the old alert there. The only thing the new API adds in those countries is the Additional Information button. Elsewhere in the EU the full-page sheet is something you opt into.

Writing a Markdown Purpose String

The full-page sheet can show richer text than the alert. You provide it with a new Info.plist key, NSUserTrackingMarkdownUsageDescription, which supports bold and italic text, bullet lists, and paragraph breaks. Underline isn't supported.

The new key doesn't replace the old one. Apple still requires NSUserTrackingUsageDescription for every ATT request, and the system falls back to it whenever the Markdown key is missing. The standard alert, which is still what everyone outside the EU sees, only ever uses the plain string.

XML
<key>NSUserTrackingUsageDescription</key>
<string>We use this to show you ads that match your interests and to learn which of our ads work.</string>
<key>NSUserTrackingMarkdownUsageDescription</key>
<string>If you allow it, we'll combine your activity in this app with data from other companies so we can:

- show you **fewer, more relevant** ads
- measure which of our ads lead to a purchase

You can change this at any time in *Settings*.</string>

Line breaks inside the <string> element carry through, so a blank line gives you a paragraph break. The system already shows your app's name in the sheet, so you don't need to repeat it. You can localize this key the same way as your other purpose strings.

Calling the New API

The new method is requestTrackingAuthorization(usingExpandedInterface:additionalInformationAction:completionHandler:), with an async variant that returns the status directly. It's only available on iOS 27.2 and later, so check availability and fall back to the existing call on older systems.

Swift
import SwiftUI
import AppTrackingTransparency

@MainActor
@Observable
final class TrackingConsent {
    var status = ATTrackingManager.trackingAuthorizationStatus
    var isShowingDetails = false
    private var shouldAskAfterDetails = false

    func requestPermission() async {
        if #available(iOS 27.2, *) {
            status = await ATTrackingManager.requestTrackingAuthorization(
                usingExpandedInterface: true,
                additionalInformationAction: { @Sendable [weak self] in
                    Task { @MainActor in
                        self?.isShowingDetails = true
                    }
                }
            )
        } else {
            status = await ATTrackingManager.requestTrackingAuthorization()
        }
    }

    func continueFromDetails() {
        shouldAskAfterDetails = true
        isShowingDetails = false
    }

    func detailsDismissed() async {
        guard shouldAskAfterDetails else { return }
        shouldAskAfterDetails = false
        await requestPermission()
    }
}

Passing true for usingExpandedInterface asks for the full-page sheet wherever the system allows it. Passing nil for additionalInformationAction hides the button entirely, so only provide an action if you have something useful to show.

The closure is marked @Sendable on purpose. Apple doesn't document which thread runs the action, and in Swift 6 a closure written inside a main actor type would otherwise inherit main actor isolation. Swift checks that isolation at runtime when Objective-C code calls the closure, so a call from a background queue could crash your app. Hopping to the main actor inside a Task keeps the UI update safe wherever the call comes from.

Handling Additional Information

When someone taps Additional Information, the system dismisses the sheet without recording an answer and runs your action. The request still finishes, and the status you get back is .notDetermined, because the person hasn't decided yet. From there it's up to you to explain your data use in your own UI, then ask again.

Here's a view that wires the model into that flow. The details sheet only asks a second time if the person taps Continue. Swiping it away leaves things alone until the next natural moment.

Swift
struct PersonalizedOffersView: View {
    @State private var consent = TrackingConsent()

    var body: some View {
        VStack(spacing: 16) {
            Text("Offers picked for you")
                .font(.title2.bold())

            Text("Next, iOS will ask whether we can use your activity to personalize ads.")
                .multilineTextAlignment(.center)
                .foregroundStyle(.secondary)

            Button("Continue") {
                Task { await consent.requestPermission() }
            }
            .buttonStyle(.borderedProminent)
        }
        .padding(24)
        .sheet(isPresented: $consent.isShowingDetails, onDismiss: {
            Task { await consent.detailsDismissed() }
        }) {
            TrackingDetailsView(onContinue: consent.continueFromDetails)
        }
    }
}

struct TrackingDetailsView: View {
    let onContinue: () -> Void

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(alignment: .leading, spacing: 12) {
                    Text("We share an advertising identifier and your in-app activity with our ad partners. They use it to decide which ads to show you and to report which ads led to a purchase.")
                    Text("Saying no doesn't change anything else in the app.")
                }
                .padding()
            }
            .navigationTitle("How We Use Tracking")
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button("Continue", action: onContinue)
                }
            }
        }
    }
}

Asking again from onDismiss rather than straight from the button means the system prompt doesn't try to appear while your sheet is still animating away. Keep the explanation honest and neutral, too. Apple's user privacy page points to App Review Guideline 5.1.2(i), which rules out gating features on tracking or offering rewards for saying yes, and a details screen is the easiest place to slip into doing exactly that.

If your code still uses completion handlers, the same method takes a completionHandler: argument. The same advice about the action closure applies there.

Asking Again After a Year

This is the bigger change for anyone whose revenue depends on ads. Until now, a person's answer to the ATT prompt was effectively permanent from your app's side. In the EU, the system now records the date of each answer, and once a year has passed you can make another request, whether the previous answer was yes or no. This applies to both the old and the new API.

There's one exception. If the person has turned off the global tracking toggle in Settings > Privacy & Security > Tracking, you can't prompt them at all. In the EU that toggle is labeled "Allow Apps to Request to Link Your Activity Across Companies".

The API doesn't tell you whether someone is eligible for a re-prompt, and you don't need it to. The header notes that when a person can't be asked again yet, the call returns their previous decision without showing anything. That makes it safe to call again, but think about when. A prompt that pops up a year later with no context gets the same poor results as one shown on first launch, so tie the second request to the same kind of moment you picked the first time.

Testing the New Prompt

Because eligibility depends on both the device's location and the Apple Account's region, you'll only see the full-page sheet on a device that meets both conditions, and Apple doesn't document a way to force it anywhere else. Outside the EU the new method behaves exactly like the old one, which means your normal testing covers the fallback path. Spend your EU testing time on the Additional Information flow and on how your Markdown string renders, since those are the parts you can't see anywhere else.

For most apps the work here is small: add the Markdown purpose string, switch to the new call behind an availability check, and decide whether an Additional Information screen helps people make the choice. The yearly re-prompt is where the real product decision lies, and it's worth planning that moment now rather than a year from now.

Sample Project

Want to see this code in action? Check out the complete sample project on GitHub:

View on GitHub

The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.