Reputation: 1233
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
Reputation: 1233
I used a combination of .onChange
methods across a few different fields to allow all the messages to appear.
Simple flow:
.authorized
localizedDescription
View Flow
ContentView
- if authenticated, shows the user's data SeriesView
view. If not authenticated, shows the AuthenticationView
AuthenticationView
allows authentication by email/password or Apple IDHere 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
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