Skip to content

Commit

Permalink
Remove use of UserDefaults from framework and add configuration param…
Browse files Browse the repository at this point in the history
…eter for device identifier
  • Loading branch information
dennisbirchdev committed Aug 1, 2023
1 parent e32d5a8 commit 2442ad7
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 22 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ SimpleAnalytics allows you to capture user actions in your apps and submit them

See the SimpleAnalyticsDemo project for a rudimentary example of using it in an Xcode project.

## Important v3.0 note
In July 2023, Apple announced that it would require developers to report use of "required reason" APIs, including UserDefaults, beginning in the fall of 2023. [Apple Developer page](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api
)

Since the original version of Simple Analytics made calls to UserDefaults to persist an identifying string that was unique to the device across app launches, you would need to inlude that usage in your app releases going forward.

In version 3.0 and later of Simple Analytics, the API for initializing an analytics reporting session has changed. Its _setEndpoint..._ method now requires passing in a device identifier string. All use of UserDefaults have been removed from the framework.

If you are upgrading from an earlier version of Simple Analytics, you will need to update your call to the _setEndpoint_ method in your app's code. In addition you will need to devise your own method for persisting an identifier if that is something you want to include in your analytics reports. One such possibility that should meet Apple's new rules is used in the accompanying demo projects' _AnalyticsManager_ files.

## Installation
SimpleAnalytics is distributed as a Swift package, which you can load into Xcode projects using the available tools built into Xcode 11.0 and higher.

Expand Down Expand Up @@ -70,20 +80,26 @@ In order to submit your app's analytics data from users' devices to a web servic

New in version 2.0 and higher: On iOS, you also need to provide a reference to the shared application instance. SimpleAnalytics uses this to request and dispose of a background task when submitting data to your web service.

To set the endpoint on __macOS__, call the `AppAnalytics.setEndpoint(_:submissionCompletionCallback:)` method.
##### To set the endpoint on __macOS__:
call the `AppAnalytics.setEndpoint(_:, deviceID:, submissionCompletionCallback:)` method.

__Parameters:__

_urlString_: String for your web service URL.

_deviceID_: String identifying the device if desired to track in your analytics reports. See the discussion of this parameter in the _"Important v3.0 note"_ section above.

_submissionCompletionCallback_: An optional completion with no argument and no return value to signal to your macOS app that submission is complete. This can be useful to implement a strategy for terminating the app _after_ analytics submission has completed.

To set the endpoint and shared app on __iOS__, call the `AppAnalytics.setEndpoint(_:, sharedApp:)` method.
#####To set the endpoint and shared app on __iOS__:
call the `AppAnalytics.setEndpoint(_:, deviceID:, sharedApp:)` method.

__Parameters:__

_urlString_: String for your web service URL.

_deviceID_: String identifying the device if desired to track in your analytics reports. See the discussion of this parameter in the _"Important v3.0 note"_ section above.

_sharedApp_: Pass the __UIApplication.shared__ property to this argument.

This call should be made as early as possible in your app's lifecycle.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

@implementation AnalyticsManager

// To test this with your own back-end setup, change the following line to reflect the URL string for your analytics server
NSString *webServiceURLString = @"https://analytics.example.com";

+ (void)initializeEndpoint {
[AppAnalytics setEndpoint:@"URL FOR YOUR WEB SERVICE" submissionCompletionCallback: nil];
[AppAnalytics setEndpoint:webServiceURLString deviceID: [AnalyticsManager deviceIdentifier] submissionCompletionCallback: nil];
}

+ (void)addCounter:(NSString *) name {
Expand All @@ -23,5 +26,40 @@ + (void)addAnalyticsItem:(NSString *) name details:(nullable NSDictionary *) det
NSLog(@"Current analytics item count: %ld", count);
}

+ (NSString *)deviceIdentifier {
NSFileManager *fileMgr = [NSFileManager defaultManager];
NSURL *folderURL = [fileMgr URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask][0];
NSString *demoFolderName = @"SimpleAnalyticsDemo";
NSString *fileName = @"Device Identifier";
NSURL *idSupportFolder = [folderURL URLByAppendingPathComponent: demoFolderName];
NSError *error = nil;
// make sure folder exists
if ([fileMgr fileExistsAtPath:[idSupportFolder path]] == NO) {
[fileMgr createDirectoryAtURL:idSupportFolder withIntermediateDirectories:YES attributes:nil error:&error];
if (error != nil) {
NSLog(@"%@", @"Failed to create the device identifier directory");
return @"";
}
}
// check to see if file exists
NSURL *fileURL = [idSupportFolder URLByAppendingPathComponent:fileName];
if ([fileMgr fileExistsAtPath:[fileURL path]] == NO) {
NSString *idString = [[NSUUID UUID] UUIDString];
[idString writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error != nil) {
NSLog(@"%@", @"Writing device identifier to disk failed.");
return @"";;
}

return idString;
} else {
NSString *idString = [NSString stringWithContentsOfURL:fileURL encoding:(NSUTF8StringEncoding) error:&error];
if (idString == nil) {
return @"";
}

return idString;
}
}

@end
46 changes: 44 additions & 2 deletions SimpleAnalyticsDemo/Shared/AnalyticsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ import UIKit
Only a few methods required for this demo are included here.
*/

// To test this with your own back-end setup, change the following line to reflect the URL string for your analytics server
let webServiceURL = "https://analytics.example.com"

struct DemoAnalytics {
static func initializeEndpoint(submissionCompletion: (() -> Void)? = nil) {
#if os(macOS)
AppAnalytics.setEndpoint("URL FOR YOUR WEB SERVICE", submissionCompletionCallback: submissionCompletion)
AppAnalytics.setEndpoint(webServiceURL, deviceID: deviceID, submissionCompletionCallback: submissionCompletion)
#elseif os(iOS)
AppAnalytics.setEndpoint("URL FOR YOUR WEB SERVICE", sharedApp: UIApplication.shared)
AppAnalytics.setEndpoint(webServiceURL, sharedApp: UIApplication.shared)
#endif
}

Expand All @@ -38,4 +41,43 @@ struct DemoAnalytics {
static func submit() {
AppAnalytics.submitNow()
}

private static var deviceID: String {
// read device ID from a file in the user's app support folder, or create it and persist it for later retrieval
let fileName = "Device Identifier"
let fileURL = appSupportFolder.appendingPathComponent(fileName)

let fileMgr = FileManager.default
if fileMgr.fileExists(atPath: fileURL.path) {
let uuid = try? String(contentsOf: fileURL, encoding: .utf8)
if let deviceID = uuid {
return deviceID
} else {
fatalError("Failed to read device ID from file")
}
} else {
let uuid = UUID().uuidString
try? uuid.write(to: fileURL, atomically: true, encoding: .utf8)
if fileMgr.fileExists(atPath: fileURL.path) == false { fatalError("Failed to save device ID file") }

return uuid
}
}

private static var appSupportFolder: URL {
let fileMgr = FileManager.default
guard let folder = fileMgr.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
fatalError()
}
if fileMgr.fileExists(atPath: folder.path) == false { fatalError("App support folder missing") }

let demoFolderURL = folder.appendingPathComponent("SimpleAnalyticsDemo")
var isFolder = true as ObjCBool
if fileMgr.fileExists(atPath: demoFolderURL.path, isDirectory: &isFolder) == false {
try? fileMgr.createDirectory(at: demoFolderURL, withIntermediateDirectories: true)
}
if fileMgr.fileExists(atPath: demoFolderURL.path) == false { fatalError("Failed to create folder") }

return demoFolderURL
}
}
22 changes: 9 additions & 13 deletions Sources/SimpleAnalytics/AppAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import os.log
private var systemVersion: String
private var shouldSubmitAtAppDismiss = true

private static var shared = AppAnalytics()
private static var shared = AppAnalytics(deviceID: "")
private static let persistenceFileName = "PersistedAnalytics"

#if os(iOS)
Expand Down Expand Up @@ -79,12 +79,14 @@ import os.log

/// Static method to set the *endPoint* and *submissionCompletionCallback* properties
/// - Parameter urlString: String for the endpoint's URL
/// - Parameter deviceID: String identifying the device
/// - Parameter submissionCompletionCallback: An optional completion with no argument and no return value, to signal to your macOS app that submission is complete
///
/// This method is required for configuring the framework on macOS.
@available (iOS, deprecated: 13.0, message: "Please use the setEndpoint(_:, sharedApp:) method")
@objc public static func setEndpoint(_ urlString: String, submissionCompletionCallback: (() -> Void)? = nil) {
@objc public static func setEndpoint(_ urlString: String, deviceID: String, submissionCompletionCallback: (() -> Void)? = nil) {
shared.endpoint = urlString
shared.deviceID = deviceID
#if os(macOS)
shared.submissionCompletionCallback = submissionCompletionCallback
#endif
Expand All @@ -93,11 +95,13 @@ import os.log
#if os(iOS)
/// Static method to set the *endPoint* and *sharedApp* properties
/// - Parameter urlString: String for the endpoint's URL
/// - Parameter deviceID: String identifying the device
/// - Parameter sharedApp: The shared UIApplication that should be used for managing background tasks. Pass in UIApplication.shared to this argument.
///
/// This method should be used for configuring the framework in iOS.
@objc public static func setEndpoint(_ urlString: String, sharedApp: UIApplication?) {
@objc public static func setEndpoint(_ urlString: String, deviceID: String, sharedApp: UIApplication?) {
shared.endpoint = urlString
shared.deviceID = deviceID
shared.sharedUIApp = sharedApp
}
#endif
Expand Down Expand Up @@ -197,7 +201,7 @@ import os.log
// MARK: - Internal & Private Methods
// MARK: -

init(endpoint: String = "", appName: String = "", applicationVersion: String = "") {
init(deviceID: String, endpoint: String = "", appName: String = "", applicationVersion: String = "") {
self.endpoint = endpoint

var name = appName
Expand Down Expand Up @@ -228,15 +232,7 @@ import os.log

self.appVersion = version

let analyticsID = "App Analytics Identifier"
if let identifier = UserDefaults.standard.string(forKey: analyticsID) {
self.deviceID = identifier
} else {
let identifier = UUID().uuidString
UserDefaults.standard.set(identifier, forKey: analyticsID)
self.deviceID = identifier
}

self.deviceID = deviceID
maxItemCount = baseItemCount

#if os(iOS)
Expand Down
4 changes: 2 additions & 2 deletions Tests/SimpleAnalyticsTests/SimpleAnalyticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
private let moveSquare = "move square"
private let jumpFive = "jump 5"

var manager = AppAnalytics(endpoint: "", appName: "")
var manager = AppAnalytics(deviceID: UUID().uuidString, endpoint: "", appName: "")

override func setUp() {
manager = AppAnalytics(endpoint: endpoint, appName: appName)
manager = AppAnalytics(deviceID: UUID().uuidString, endpoint: endpoint, appName: appName)
}

func testNameAndEndpoint() {
Expand Down
4 changes: 2 additions & 2 deletions Tests/SimpleAnalyticsTests/SubmissionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ final class SubmissionTests: XCTestCase {
private let moveSquare = "move square"
private let jumpFive = "jump 5"

var manager = AppAnalytics(endpoint: "", appName: "")
var manager = AppAnalytics(deviceID: UUID().uuidString, endpoint: "", appName: "")

override func setUp() {
manager = AppAnalytics(endpoint: endpoint, appName: appName)
manager = AppAnalytics(deviceID: UUID().uuidString, endpoint: endpoint, appName: appName)
}

func testSubmitSuccess() {
Expand Down

0 comments on commit 2442ad7

Please sign in to comment.