Jacksonsox
Jacksonsox

Reputation: 1233

ATTrackingManager.requestTrackingAuthorization not showing on Not Determined

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)

Upvotes: 2

Views: 776

Answers (2)

Jacksonsox
Jacksonsox

Reputation: 1233

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)
            }
        }
}

Upvotes: 0

Om Sanatan
Om Sanatan

Reputation: 130

    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.

Upvotes: 1

Related Questions