skip to Main Content

I’m trying to implement the ATTrackingManager.requestTrackingAuthorization in my Swift app. I saw the tracking usage description appear once, but haven’t seen it since. Based on another post, I think that since I’m changing the State variables after an .onAppear() the message is either not being displayed or displaying and then being removed/overwritten.

I’ve set Privacy - Tracking Usage Description in the info.plist

I tried to put the ATT call on an .onAppear in the forest group that is displayed. One an Apple technical forum, I saw that if the result returned is "Not Determined", call ATTrackingManager.requestTrackingAuthorization a second time.

It seems like I should be calling the requestTrackingAuthorization from a different place than onAppear and I’m making the process way to complicated.

What I need to happen:
I need the ATT check to happen every time the user opens the app to support new installs and existing installs and the message to display when the value is .notDetermined. The user needs to be able to make a selection (Track/Not Track) before entering any authentication data AuthentiationView() or as the user is already authenticated and presented with SeriesTabs(selection: $selection)

Code showing the .onAppear usage and the ContentView

import SwiftUI
import UIKit
import Firebase

@main
struct MyToyBoxApp: App {
    @StateObject private var modelData = ModelData()
    
    init() {
      FirebaseApp.configure()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(modelData) // app data
                .environmentObject(AuthenticationState.shared) // firebase
        }
    }
}
import SwiftUI
import AppTrackingTransparency

enum Tab {
    case photos
    case list
}

struct ContentView: View {
    @EnvironmentObject var authState: AuthenticationState
    
    @State private var selection: Tab = .photos
    
    var body: some View {
        Group {
            if authState.loggedInUser != nil {
                SeriesTabs(selection: $selection)
            } else {
                AuthentiationView()
            }
        }
        .onAppear() {
            if let status = determineDataTrackingStatus() {
                if status == .notDetermined {
                    print("First call status (status)")
                    let statusSecondCall = determineDataTrackingStatus()
                    print ("Second call status (String(describing: statusSecondCall))")
                }
            }
        }
    }
    
    func determineDataTrackingStatus() -> ATTrackingManager.AuthorizationStatus? {
        var authorizationStatus: ATTrackingManager.AuthorizationStatus?
        
        if #available(iOS 14, *) {
            
            ATTrackingManager.requestTrackingAuthorization { status in
                authorizationStatus = status
                
                switch status {
                case .authorized:
                    // Tracking authorization dialog was shown
                    // and we are authorized
                    print("Authorized")
                case .denied:
                    // Tracking authorization dialog was
                    // shown and permission is denied
                    print("Denied")
                case .notDetermined:
                    // Tracking authorization dialog has not been shown
                    print("Not Determined")
                case .restricted:
                    print("Restricted")
                @unknown default:
                    print("Unknown")
                }
            }
        } else {
            // do nothing
        }
        
        return (authorizationStatus)
        

    }
}

I’ve also checked this post, but I’m not sure where ApplicationDidBecomeActive needs to be set or what its overriding as there is no override mentioned.

our complete guide to ATT (updated for iOS 15)

2

Answers


  1. Chosen as BEST ANSWER

    I used a combination of .onChange methods across a few different fields to allow all the messages to appear.

    Simple flow:

    1. Check if Data Tracking isn't .authorized
    2. If not authorized, show a user message letting them know the application doesn't work without data tracking (i.e., authentication and user data capture using Firebase Frameworks
    3. If the user attempts to sign in with invalid credentials, a message displays that the credentials are invalid based on the localizedDescription

    View Flow

    • ContentView - if authenticated, shows the user's data SeriesView view. If not authenticated, shows the AuthenticationView
    • The AuthenticationView allows authentication by email/password or Apple ID

    Here are a few examples of what I ended up using:

    struct ContentView: View { 
        @EnvironmentObject var trackingState : ATTrackingManagerState
        @Environment(.scenePhase) var scenePhase
        @State var isTrackingUnauthorized = false // assume authorized
    
    var body: some View {
        Group {
            if authState.loggedInUser != nil {
                SeriesTabs(selection: $selection) //<-- SeriesView
            } else {
                AuthentiationView() //<-- AuthenticationView (for email/password or AppleID
            }
        }
    }
    

    On the SeriesView, I needed the following onChange of the scenePhase watching for the .active phase

                SeriesPhotoView()
                .tabItem {
                    Label("Photos", systemImage: "photo")
                }
                .onChange(of: scenePhase) { newPhase in
                    if newPhase == .active {
                        checkTrackingAuthStatus()
                    }
                }
                .alert(isPresented: $isTrackingUnauthorized) {
                    print("Data tracking denied message")
                    return Alert(title: Text("We value your Privacy"), message: Text(kATTrackingMessage), dismissButton: .default(Text("Got it!"), action: {authState.signout()}))
                }
    

    Then for the appleID auth check:

    struct AuthentiationView: View {
    
    @EnvironmentObject var authState: AuthenticationState
    @State var authType = AuthenticationType.login
    
    @EnvironmentObject var trackingState : ATTrackingManagerState
    @Environment(.scenePhase) var scenePhase
    @State var isTrackingUnauthorized = false // assume authorized
    
    var body: some View {
        ZStack {
            VStack(spacing: 32) {
                LogoTitle()
                if (!authState.isAuthenticating) {
                    SignInAppleButton {
                        checkTrackingAuthStatus() { status in
                            
                            // if the status is authorized, attempt login
                            if status == .authorized {
                                self.authState.login(with: .signInWithApple)
                            }
                        }
                    }
                    .frame(width: 130, height: 44)
                    .alert(isPresented: $isTrackingUnauthorized) {
                        print("Data tracking denied message")
                        return Alert(title: Text("We value your Privacy"), message: Text(kATTrackingMessage), dismissButton: .default(Text("Got it!"), action: {authState.signout()}))
                    }
    

    The next bit of code seemed a bit sloppy, but worked. When the email field changes and/or was active, I checked the tracking authorization

    struct AuthenticationFormView: View {
        @EnvironmentObject var trackingState : ATTrackingManagerState
        @Environment(.scenePhase) var scenePhase
        @State var isEmailFocused:Bool = false
        @State var isTrackingUnauthorized = false // assume authorize
        ...
    
                TextField("Email", text: $email)
                .textContentType(.emailAddress)
                .keyboardType(.emailAddress)
                .autocapitalization(.none)
                .onChange(of: scenePhase) { newPhase in
                    if newPhase == .active {
                        checkTrackingAuthStatus(emailValue: email)
                    }
                }
                .onChange(of: email){ emailValue in
                    checkTrackingAuthStatus(emailValue: email)
                }
                .alert(isPresented: $isTrackingUnauthorized) {
                    print("Data tracking denied message")
                    return Alert(title: Text("We value your Privacy"), message: Text(kATTrackingMessage), dismissButton: .default(Text("Got it!"), action: {authState.signout()}))
                }
    

    Next is an example of the checkTrackingAuthStatus. All 3 calls were similar with slight variation.

        private func checkTrackingAuthStatus(completionHandler completion: @escaping (ATTrackingManager.AuthorizationStatus?) -> Void) {
        // if tracking not authorized, call for status and show message
        trackingState.requestTrackingAuthorization(completionHandler: {status in
            trackingState.aTTrackingManagerStatus = status
            
                if trackingState.aTTrackingManagerStatus != .authorized {
                    isTrackingUnauthorized = true
                } else {
                    isTrackingUnauthorized = false
                }
            
            completion(status)
        })
    

    @Om, as you noted above, the trackingState trackingStatus call which didn't need the asynchAfter call

        func requestTrackingAuthorization(completionHandler completion: @escaping (ATTrackingManager.AuthorizationStatus?) -> Void) {
        if #available(iOS 14, *) {
    
                ATTrackingManager.requestTrackingAuthorization { status in
                    
                    self.aTTrackingManagerStatus = status
                    
                    switch status {
                    case .authorized:
                        // Tracking authorization dialog was shown
                        // and we are authorized
                        print("Authorized")
                    case .denied:
                        // Tracking authorization dialog was denied
                        print("Denied")
                    case .notDetermined:
                        // Tracking authorization dialog has not been shown
                        print("Not Determined")
                    case .restricted:
                        print("Restricted")
                    @unknown default:
                        print("Not Approved")
                    }
                    
                    completion (status)
                }
            }
    }
    

  2.     import AppTrackingTransparency
        
        var body: some View {
            Group {
                if authState.loggedInUser != nil {
                    SeriesTabs(selection: $selection)
                } else {
                    AuthentiationView()
                }
            }
            .onAppear() {
                ATTTrackingDialougue() // Just call it directly
            }
        }
        
        func ATTTrackingDialougue() {
            ATTrackingManager.requestTrackingAuthorization { status in
                switch status {
                case .authorized:
                    // Tracking authorization dialog was shown
                    // and we are authorized
                    print("Authorized")
                case .denied:
                    // Tracking authorization dialog was
                    // shown and permission is denied
                    print("Denied")
                case .notDetermined:
                    // Tracking authorization dialog has not been shown
                    print("Not Determined")
                case .restricted:
                    print("Restricted")
                @unknown default:
                    print("Unknown")
                }
            }
        }
    

    There is no need to check status on appear ATTTrackingDialougue should called without checking status.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search