```html SwiftUI: How to Implement App Tracking Transparency (iOS 17+, 2026)

How to implement App Tracking Transparency in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: AppTrackingTransparency Updated: May 11, 2026
TL;DR

Add NSUserTrackingUsageDescription to your Info.plist, then call ATTrackingManager.requestTrackingAuthorization(completionHandler:) inside an .onReceive(NotificationCenter…) handler that fires when the scene becomes active — the system presents the ATT prompt exactly once.

import AppTrackingTransparency

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .onReceive(
                NotificationCenter.default.publisher(
                    for: UIApplication.didBecomeActiveNotification)
            ) { _ in
                Task {
                    await ATTrackingManager
                        .requestTrackingAuthorization()
                }
            }
    }
}

Full implementation

The implementation wraps ATT state in an @Observable class so your entire SwiftUI hierarchy can react to changes in authorization status. The prompt is triggered the first time the app transitions to the active state after install; on subsequent launches ATTrackingManager.trackingAuthorizationStatus returns the cached answer without re-prompting, so we read that synchronously on startup and only call the async authorization request when the status is still notDetermined.

// TrackingManager.swift
import AppTrackingTransparency
import Observation

@Observable
final class TrackingManager {

    // MARK: – Published state
    var status: ATTrackingManager.AuthorizationStatus = .notDetermined

    // Convenience booleans for UI bindings
    var isAuthorized: Bool { status == .authorized }
    var isDenied: Bool     { status == .denied || status == .restricted }

    // MARK: – Initialiser – read cached status immediately
    init() {
        status = ATTrackingManager.trackingAuthorizationStatus
    }

    // MARK: – Request (only shows OS prompt when status == .notDetermined)
    @MainActor
    func requestAuthorization() async {
        guard status == .notDetermined else { return }
        status = await ATTrackingManager.requestTrackingAuthorization()
    }
}

// ContentView.swift
import SwiftUI
import AppTrackingTransparency

struct ContentView: View {
    @Environment(TrackingManager.self) private var tracking

    var body: some View {
        NavigationStack {
            VStack(spacing: 24) {
                TrackingStatusBadge(status: tracking.status)

                if tracking.isDenied {
                    VStack(spacing: 12) {
                        Text("Tracking permission denied.")
                            .font(.subheadline)
                            .foregroundStyle(.secondary)
                        Button("Open Settings") {
                            guard let url = URL(string: UIApplication.openSettingsURLString)
                            else { return }
                            UIApplication.shared.open(url)
                        }
                        .buttonStyle(.borderedProminent)
                    }
                }
            }
            .padding()
            .navigationTitle("Privacy")
        }
        // Trigger on every foreground transition; requestAuthorization
        // is a no-op once the status is no longer .notDetermined.
        .onReceive(
            NotificationCenter.default.publisher(
                for: UIApplication.didBecomeActiveNotification)
        ) { _ in
            Task {
                await tracking.requestAuthorization()
            }
        }
    }
}

// TrackingStatusBadge.swift
import SwiftUI
import AppTrackingTransparency

struct TrackingStatusBadge: View {
    let status: ATTrackingManager.AuthorizationStatus

    private var label: String {
        switch status {
        case .authorized:    return "Authorized"
        case .denied:        return "Denied"
        case .restricted:    return "Restricted"
        case .notDetermined: return "Not Determined"
        @unknown default:    return "Unknown"
        }
    }

    private var color: Color {
        switch status {
        case .authorized:    return .green
        case .denied:        return .red
        case .restricted:    return .orange
        case .notDetermined: return .secondary
        @unknown default:    return .secondary
        }
    }

    var body: some View {
        Label(label, systemImage: "hand.raised.fill")
            .foregroundStyle(color)
            .padding(.horizontal, 14)
            .padding(.vertical, 8)
            .background(color.opacity(0.12), in: Capsule())
            .accessibilityLabel("Tracking status: \(label)")
    }
}

// App entry point
import SwiftUI

@main
struct MyApp: App {
    @State private var tracking = TrackingManager()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(tracking)
        }
    }
}

#Preview {
    ContentView()
        .environment(TrackingManager())
}

How it works

  1. TrackingManager reads the cached status on init. ATTrackingManager.trackingAuthorizationStatus is synchronous and returns the previously stored decision, so the UI renders with the correct state before the scene even becomes active.
  2. .onReceive(UIApplication.didBecomeActiveNotification) fires the request. Hooking into the active-state notification (rather than onAppear) guarantees the ATT alert appears after the app's own UI is fully visible — Apple's HIG requirement — and re-fires harmlessly on subsequent launches because requestAuthorization() early-returns when status != .notDetermined.
  3. @Observable TrackingManager propagates state app-wide. Injected via .environment(tracking) at the root, any child view that reads tracking.isAuthorized automatically re-renders when the status changes — no extra @Published or ObservableObject boilerplate needed in iOS 17+.
  4. The "Open Settings" deep-link handles the denied case. UIApplication.openSettingsURLString takes the user directly to your app's Privacy settings page, where they can flip the switch without leaving the flow.
  5. @unknown default future-proofs the switch statements. Apple may add new AuthorizationStatus cases in future OS releases; exhaustive @unknown default branches prevent compiler warnings and unhandled crashes.

Variants

Delay the prompt until after onboarding

Show the ATT prompt after your own onboarding screen so users understand why you're asking before the OS dialog appears. Gate the request behind a boolean preference.

// In TrackingManager
var onboardingComplete: Bool {
    get { UserDefaults.standard.bool(forKey: "onboardingComplete") }
    set { UserDefaults.standard.set(newValue, forKey: "onboardingComplete") }
}

@MainActor
func requestIfReady() async {
    guard onboardingComplete else { return }
    await requestAuthorization()
}

// In your onboarding completion handler:
Button("Get Started") {
    tracking.onboardingComplete = true
    Task { await tracking.requestAuthorization() }
}

Check status before making an ad SDK call

Many ad SDKs (Google AdMob, Meta Audience Network) require you to pass the IDFA only when status == .authorized. Wrap the SDK initialisation call inside a status check so the SDK degrades gracefully to contextual ads on denial:

import AdSupport

func configureAdSDK(status: ATTrackingManager.AuthorizationStatus) {
    let idfa = status == .authorized
        ? ASIdentifierManager.shared().advertisingIdentifier.uuidString
        : "00000000-0000-0000-0000-000000000000" // zeroed-out IDFA

    // Pass idfa to your ad SDK initialiser here
    print("Configuring ads with IDFA:", idfa)
}

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement app tracking transparency in SwiftUI for iOS 17+.
Use AppTrackingTransparency and ATTrackingManager.
Create an @Observable TrackingManager injected via .environment().
Trigger requestTrackingAuthorization() on UIApplication.didBecomeActiveNotification.
Handle all ATTrackingManager.AuthorizationStatus cases including @unknown default.
Show an "Open Settings" button in the denied state.
Make it accessible (VoiceOver labels on the status badge).
Add NSUserTrackingUsageDescription to Info.plist with a user-facing string.
Add a #Preview with realistic sample data.

Drop this prompt into the Soarias Build phase after your screen scaffolding is generated — ATT wiring is a single-session task that slots cleanly between UI build-out and App Store submission prep.

Related

FAQ

Does App Tracking Transparency work on iOS 16?

The AppTrackingTransparency framework was introduced in iOS 14, so the framework itself compiles and runs on iOS 16. However, this guide targets iOS 17+ because it uses the @Observable macro (iOS 17+) and the await ATTrackingManager.requestTrackingAuthorization() async/await overload added in iOS 16. If you need iOS 15 support, use the callback-based requestTrackingAuthorization(completionHandler:) variant and replace @Observable with ObservableObject + @Published.

Does my app need ATT even if I don't use third-party ad SDKs?

You need ATT any time your app (or a third-party SDK embedded in it) accesses ASIdentifierManager.advertisingIdentifier (the IDFA), tracks users across apps or websites you don't own, or links behavioral data to a third-party identity. If you use analytics SDKs like Firebase, Mixpanel, or any ad network framework — even for attribution only — you almost certainly need ATT. Apps that collect zero cross-app data and don't read the IDFA can omit the entire flow, but you must still declare this accurately in App Store Connect's privacy nutrition labels.

What is the UIKit equivalent of this SwiftUI approach?

In UIKit you call the same ATTrackingManager API, but trigger it from applicationDidBecomeActive(_:) in your AppDelegate, or from sceneDidBecomeActive(_:) in a UIWindowSceneDelegate. The framework and status enum are identical; only the call site differs.

// UIKit – SceneDelegate.swift
import AppTrackingTransparency

func sceneDidBecomeActive(_ scene: UIScene) {
    Task {
        let status = await ATTrackingManager
            .requestTrackingAuthorization()
        print("ATT status:", status)
    }
}

Last reviewed: 2026-05-11 by the Soarias team.

```