Skip to content

Commit c8252bc

Browse files
authored
feat: allow any dimension value for ParseAnalytics (#341)
* feat: allow any dimension value for ParseAnalytics * nit changelog * nits * Update .codecov.yml * coverage * more coverage * improve
1 parent 5307210 commit c8252bc

15 files changed

+224
-224
lines changed

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22

33
### main
44

5-
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.0.1...main)
5+
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.1.0...main)
66
* _Contributing to this repo? Add info about your change here to be included in the next release_
77

8+
### 4.1.0
9+
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.0.1...4.1.0)
10+
11+
__Improvements__
12+
- Let the OS and developer decide if app tracking authorization is required when using ParseAnalytics. ParseAnalytics can now take any Codable value in its' dimensions instead of just strings. Added a new property "date" to ParseAnalytics. The "at" property will be deprecated in ParseSwift 5.0.0, so developers should switch to "date". ParseAnalytics can now be properly decoded after encoding with a JSONEncoder. This is useful if ParseAnalytics need to be stored locally and sent to the server later ([#341](https://github.com/parse-community/Parse-Swift/pull/341)), thanks to [Corey Baker](https://github.com/cbaker6).
13+
814
### 4.0.1
915
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.0.0...4.0.1)
1016

@@ -21,7 +27,7 @@ __New features__
2127
- Add DocC for SDK documentation ([#209](https://github.com/parse-community/Parse-Swift/pull/214)), thanks to [Corey Baker](https://github.com/cbaker6).
2228

2329
__Improvements__
24-
- (Breaking Change) Make ParseRelation conform to Codable and add methods to make decoded stored ParseRelations "usable". ParseObjects can now contain properties of ParseRelation<Self>. In addition, ParseRelations can now be made from ParseObject pointers. For ParseRole, the computed properties: users and roles, are now optional. The queryRoles property has been changed to queryRoles() to improve the handling of thrown errors ([#328](https://github.com/parse-community/Parse-Swift/pull/328)), thanks to @cbaker6.
30+
- (Breaking Change) Make ParseRelation conform to Codable and add methods to make decoded stored ParseRelations "usable". ParseObjects can now contain properties of ParseRelation<Self>. In addition, ParseRelations can now be made from ParseObject pointers. For ParseRole, the computed properties: users and roles, are now optional. The queryRoles property has been changed to queryRoles() to improve the handling of thrown errors ([#328](https://github.com/parse-community/Parse-Swift/pull/328)), thanks to [Corey Baker](https://github.com/cbaker6).
2531
- (Breaking Change) Change the following method parameter names: isUsingMongoDB -> usingMongoDB, isIgnoreCustomObjectIdConfig -> ignoringCustomObjectIdConfig, isUsingEQ -> usingEqComparator ([#321](https://github.com/parse-community/Parse-Swift/pull/321)), thanks to [Corey Baker](https://github.com/cbaker6).
2632
- (Breaking Change) Change the following method parameter names: isUsingMongoDB -> usingMongoDB, isIgnoreCustomObjectIdConfig -> ignoringCustomObjectIdConfig, isUsingEQ -> usingEqComparator ([#321](https://github.com/parse-community/Parse-Swift/pull/321)), thanks to [Corey Baker](https://github.com/cbaker6).
2733
- (Breaking Change) Change the following method parameter names: isUsingTransactions -> usingTransactions, isAllowingCustomObjectIds -> allowingCustomObjectIds, isUsingEqualQueryConstraint -> usingEqualQueryConstraint, isMigratingFromObjcSDK -> migratingFromObjcSDK, isDeletingKeychainIfNeeded -> deletingKeychainIfNeeded ([#323](https://github.com/parse-community/Parse-Swift/pull/323)), thanks to [Corey Baker](https://github.com/cbaker6).

ParseSwift.xcodeproj/project.pbxproj

Lines changed: 21 additions & 21 deletions
Large diffs are not rendered by default.

Sources/ParseSwift/Coding/ParseEncoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ public struct ParseEncoder {
191191
}
192192

193193
// MARK: _ParseEncoder
194-
private class _ParseEncoder: JSONEncoder, Encoder {
194+
internal class _ParseEncoder: JSONEncoder, Encoder {
195195
var codingPath: [CodingKey]
196196
let dictionary: NSMutableDictionary
197197
let skippedKeys: Set<String>

Sources/ParseSwift/LiveQuery/ParseLiveQuery+async.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import Foundation
1111

1212
extension ParseLiveQuery {
13-
// MARK: Async/Await
13+
// MARK: Connection - Async/Await
1414

1515
/**
1616
Manually establish a connection to the `ParseLiveQuery` Server.

Sources/ParseSwift/LiveQuery/ParseLiveQuery+combine.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Foundation
1111
import Combine
1212

1313
extension ParseLiveQuery {
14-
// MARK: Combine
14+
// MARK: Connection - Combine
1515

1616
/**
1717
Manually establish a connection to the `ParseLiveQuery` Server. Publishes when established.

Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ Not attempting to open ParseLiveQuery socket anymore
223223
}
224224
}
225225

226-
// MARK: Helpers
227226
extension ParseLiveQuery {
228227

229228
/// Current LiveQuery client.

Sources/ParseSwift/Objects/ParseRole.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ public extension ParseRole {
9898
}
9999

100100
static func == (lhs: Self, rhs: Self) -> Bool {
101-
lhs.name == rhs.name && lhs.className == rhs.className
101+
lhs.debugDescription == rhs.debugDescription
102102
}
103103

104104
func hash(into hasher: inout Hasher) {
105-
hasher.combine("\(self.className)_\(String(describing: self.name))")
105+
hasher.combine(self.debugDescription)
106106
}
107107
}
108108

Sources/ParseSwift/ParseConstants.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
enum ParseConstants {
1212
static let sdk = "swift"
13-
static let version = "4.0.1"
13+
static let version = "4.1.0"
1414
static let fileManagementDirectory = "parse/"
1515
static let fileManagementPrivateDocumentsDirectory = "Private Documents/"
1616
static let fileManagementLibraryDirectory = "Library/"

Sources/ParseSwift/Types/ParseACL.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ public struct ParseACL: ParseType,
3232
case write
3333

3434
public init(from decoder: Decoder) throws {
35-
self = Access(rawValue: try decoder.singleValueContainer().decode(String.self))!
35+
guard let decoded = Access(rawValue: try decoder.singleValueContainer().decode(String.self)) else {
36+
throw ParseError(code: .unknownError, message: "Not able to decode ParseACL Access")
37+
}
38+
self = decoded
3639
}
3740

3841
public func encode(to encoder: Encoder) throws {

Sources/ParseSwift/Types/ParseAnalytics+async.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,19 @@ public extension ParseAnalytics {
9696
- parameter at: Explicitly set the time associated with a given event. If not provided the
9797
server time will be used.
9898
- parameter options: A set of header options sent to the server. Defaults to an empty set.
99-
- warning: This method makes a copy of the current `ParseAnalytics` and then mutates
100-
it. You will not have access to the mutated analytic after calling this method.
10199
- throws: An error of type `ParseError`.
102100
*/
103-
func track(dimensions: [String: String]?,
104-
at date: Date? = nil,
105-
options: API.Options = []) async throws {
106-
let _: Void = try await withCheckedThrowingContinuation { continuation in
107-
var analytic = self
108-
analytic.track(dimensions: dimensions,
109-
at: date,
110-
options: options,
111-
completion: continuation.resume)
101+
mutating func track(dimensions: [String: String]?,
102+
at date: Date? = nil,
103+
options: API.Options = []) async throws {
104+
let result = try await withCheckedThrowingContinuation { continuation in
105+
self.track(dimensions: dimensions,
106+
at: date,
107+
options: options,
108+
completion: continuation.resume)
109+
}
110+
if case let .failure(error) = result {
111+
throw error
112112
}
113113
}
114114
}

Sources/ParseSwift/Types/ParseAnalytics.swift

Lines changed: 78 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,46 @@ import Foundation
1111
import UIKit
1212
#endif
1313

14-
#if canImport(AppTrackingTransparency)
15-
import AppTrackingTransparency
16-
#endif
17-
1814
/**
19-
`ParseAnalytics` provides an interface to Parse's logging and analytics
20-
backend.
21-
22-
- warning: For iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, you
23-
will need to request tracking authorization for ParseAnalytics to work.
24-
See Apple's [documentation](https://developer.apple.com/documentation/apptrackingtransparency) for more for details.
15+
`ParseAnalytics` provides an interface to Parse's logging and analytics backend.
2516
*/
2617
public struct ParseAnalytics: ParseType, Hashable {
2718

2819
/// The name of the custom event to report to Parse as having happened.
29-
public let name: String
20+
public var name: String
3021

3122
/// Explicitly set the time associated with a given event. If not provided the server
3223
/// time will be used.
33-
public var at: Date? // swiftlint:disable:this identifier_name
24+
/// - warning: This will be deprecated in ParseSwift 5.0.0 in favor of `date`.
25+
public var at: Date? { // swiftlint:disable:this identifier_name
26+
get {
27+
date
28+
}
29+
set {
30+
date = newValue
31+
}
32+
}
33+
34+
/// Explicitly set the time associated with a given event. If not provided the server
35+
/// time will be used.
36+
public var date: Date?
3437

3538
/// The dictionary of information by which to segment this event.
36-
public var dimensions: [String: String]?
39+
public var dimensions: [String: Codable]? {
40+
get {
41+
convertToString(dimensionsAnyCodable)
42+
}
43+
set {
44+
dimensionsAnyCodable = convertToAnyCodable(newValue)
45+
}
46+
}
47+
48+
var dimensionsAnyCodable: [String: AnyCodable]?
3749

3850
enum CodingKeys: String, CodingKey {
39-
case at, dimensions // swiftlint:disable:this identifier_name
51+
case date = "at"
52+
case dimensions
53+
case name
4054
}
4155

4256
/**
@@ -47,13 +61,55 @@ public struct ParseAnalytics: ParseType, Hashable {
4761
time will be used. Defaults to `nil`.
4862
*/
4963
public init (name: String,
50-
dimensions: [String: String]? = nil,
51-
at: Date? = nil) { // swiftlint:disable:this identifier_name
64+
dimensions: [String: Codable]? = nil,
65+
at date: Date? = nil) {
5266
self.name = name
53-
self.dimensions = dimensions
54-
self.at = at
67+
self.dimensionsAnyCodable = convertToAnyCodable(dimensions)
68+
self.date = date
69+
}
70+
71+
public func encode(to encoder: Encoder) throws {
72+
var container = encoder.container(keyedBy: CodingKeys.self)
73+
try container.encodeIfPresent(date, forKey: .date)
74+
try container.encodeIfPresent(dimensionsAnyCodable, forKey: .dimensions)
75+
if !(encoder is _ParseEncoder) {
76+
try container.encode(name, forKey: .name)
77+
}
78+
}
79+
80+
public static func == (lhs: Self, rhs: Self) -> Bool {
81+
lhs.debugDescription == rhs.debugDescription
82+
}
83+
84+
public func hash(into hasher: inout Hasher) {
85+
hasher.combine(self.debugDescription)
86+
}
87+
88+
// MARK: Helpers
89+
func convertToAnyCodable(_ dimensions: [String: Codable]?) -> [String: AnyCodable]? {
90+
guard let dimensions = dimensions else {
91+
return nil
92+
}
93+
var convertedDimensions = [String: AnyCodable]()
94+
for (key, value) in dimensions {
95+
convertedDimensions[key] = AnyCodable(value)
96+
}
97+
return convertedDimensions
98+
}
99+
100+
func convertToString(_ dimensions: [String: AnyCodable]?) -> [String: String]? {
101+
guard let dimensions = dimensions else {
102+
return nil
103+
}
104+
var convertedDimensions = [String: String]()
105+
for (key, value) in dimensions {
106+
convertedDimensions[key] = "\(value.value)"
107+
}
108+
return convertedDimensions
55109
}
56110

111+
// MARK: Intents
112+
57113
#if os(iOS)
58114
/**
59115
Tracks *asynchronously* this application being launched. If this happened as the result of the
@@ -79,22 +135,6 @@ public struct ParseAnalytics: ParseType, Hashable {
79135
completion: @escaping (Result<Void, ParseError>) -> Void) {
80136
var options = options
81137
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
82-
#if canImport(AppTrackingTransparency)
83-
if #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, tvOS 14.0, *) {
84-
if !ParseSwift.configuration.isTestingSDK {
85-
let status = ATTrackingManager.trackingAuthorizationStatus
86-
if status != .authorized {
87-
callbackQueue.async {
88-
let error = ParseError(code: .unknownError,
89-
// swiftlint:disable:next line_length
90-
message: "App tracking not authorized. Please request permission from user.")
91-
completion(.failure(error))
92-
}
93-
return
94-
}
95-
}
96-
}
97-
#endif
98138
var userInfo: [String: String]?
99139
if let remoteOptions = launchOptions?[.remoteNotification] as? [String: String] {
100140
userInfo = remoteOptions
@@ -118,7 +158,7 @@ public struct ParseAnalytics: ParseType, Hashable {
118158
Tracks *asynchronously* this application being launched with additional dimensions.
119159

120160
- parameter dimensions: The dictionary of information by which to segment this
121-
event and can be empty or `nil`.
161+
event. Can be empty or `nil`.
122162
- parameter at: Explicitly set the time associated with a given event. If not provided the
123163
server time will be used.
124164
- parameter options: A set of header options sent to the server. Defaults to an empty set.
@@ -135,22 +175,6 @@ public struct ParseAnalytics: ParseType, Hashable {
135175
completion: @escaping (Result<Void, ParseError>) -> Void) {
136176
var options = options
137177
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
138-
#if canImport(AppTrackingTransparency)
139-
if #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, tvOS 14.0, *) {
140-
if !ParseSwift.configuration.isTestingSDK {
141-
let status = ATTrackingManager.trackingAuthorizationStatus
142-
if status != .authorized {
143-
callbackQueue.async {
144-
let error = ParseError(code: .unknownError,
145-
// swiftlint:disable:next line_length
146-
message: "App tracking not authorized. Please request permission from user.")
147-
completion(.failure(error))
148-
}
149-
return
150-
}
151-
}
152-
}
153-
#endif
154178
let appOppened = ParseAnalytics(name: "AppOpened",
155179
dimensions: dimensions,
156180
at: date)
@@ -180,22 +204,6 @@ public struct ParseAnalytics: ParseType, Hashable {
180204
completion: @escaping (Result<Void, ParseError>) -> Void) {
181205
var options = options
182206
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
183-
#if canImport(AppTrackingTransparency)
184-
if #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, tvOS 14.0, *) {
185-
if !ParseSwift.configuration.isTestingSDK {
186-
let status = ATTrackingManager.trackingAuthorizationStatus
187-
if status != .authorized {
188-
callbackQueue.async {
189-
let error = ParseError(code: .unknownError,
190-
// swiftlint:disable:next line_length
191-
message: "App tracking not authorized. Please request permission from user.")
192-
completion(.failure(error))
193-
}
194-
return
195-
}
196-
}
197-
}
198-
#endif
199207
self.saveCommand().executeAsync(options: options,
200208
callbackQueue: callbackQueue) { result in
201209
switch result {
@@ -211,7 +219,7 @@ public struct ParseAnalytics: ParseType, Hashable {
211219
Tracks *asynchronously* the occurrence of a custom event with additional dimensions.
212220

213221
- parameter dimensions: The dictionary of information by which to segment this
214-
event and can be empty or `nil`.
222+
event. Can be empty or `nil`.
215223
- parameter at: Explicitly set the time associated with a given event. If not provided the
216224
server time will be used.
217225
- parameter options: A set of header options sent to the server. Defaults to an empty set.
@@ -228,24 +236,8 @@ public struct ParseAnalytics: ParseType, Hashable {
228236
completion: @escaping (Result<Void, ParseError>) -> Void) {
229237
var options = options
230238
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
231-
#if canImport(AppTrackingTransparency)
232-
if #available(macOS 11.0, iOS 14.0, macCatalyst 14.0, tvOS 14.0, *) {
233-
if !ParseSwift.configuration.isTestingSDK {
234-
let status = ATTrackingManager.trackingAuthorizationStatus
235-
if status != .authorized {
236-
callbackQueue.async {
237-
let error = ParseError(code: .unknownError,
238-
// swiftlint:disable:next line_length
239-
message: "App tracking not authorized. Please request permission from user.")
240-
completion(.failure(error))
241-
}
242-
return
243-
}
244-
}
245-
}
246-
#endif
247-
self.dimensions = dimensions
248-
self.at = date
239+
self.dimensionsAnyCodable = convertToAnyCodable(dimensions)
240+
self.date = date
249241
self.saveCommand().executeAsync(options: options,
250242
callbackQueue: callbackQueue) { result in
251243
switch result {

Tests/ParseSwiftTests/ParseACLTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,22 @@ class ParseACLTests: XCTestCase {
215215
}
216216
}
217217

218+
func testCodingAccess() throws {
219+
let access = ParseACL.Access.read
220+
let encoded = try ParseCoding.jsonEncoder().encode(access)
221+
let decoded = try ParseCoding.jsonDecoder().decode(ParseACL.Access.self, from: encoded)
222+
XCTAssertEqual(access, decoded)
223+
let access2 = ParseACL.Access.write
224+
let encoded2 = try ParseCoding.jsonEncoder().encode(access2)
225+
let decoded2 = try ParseCoding.jsonDecoder().decode(ParseACL.Access.self, from: encoded2)
226+
XCTAssertEqual(access2, decoded2)
227+
guard let data = "hello".data(using: .utf8) else {
228+
XCTFail("Should have unwrapped")
229+
return
230+
}
231+
XCTAssertThrowsError(try ParseCoding.jsonDecoder().decode(ParseACL.Access.self, from: data))
232+
}
233+
218234
func testDebugString() {
219235
var acl = ParseACL()
220236
acl.setReadAccess(objectId: "a", value: false)

0 commit comments

Comments
 (0)