Skip to content

Commit ccfc844

Browse files
authored
[rc-swift] Custom Signals and Sendable (#14359)
1 parent 824ffcd commit ccfc844

File tree

6 files changed

+193
-34
lines changed

6 files changed

+193
-34
lines changed

FirebaseRemoteConfig/Swift/CustomSignals.swift

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public struct CustomSignalValue {
5353
Self(kind: .double(double))
5454
}
5555

56-
fileprivate func toNSObject() -> NSObject {
56+
func toNSObject() -> NSObject {
5757
switch kind {
5858
case let .string(string):
5959
return string as NSString
@@ -82,26 +82,3 @@ extension CustomSignalValue: ExpressibleByFloatLiteral {
8282
self = .double(value)
8383
}
8484
}
85-
86-
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
87-
public extension RemoteConfig {
88-
/// Sets custom signals for this Remote Config instance.
89-
/// - Parameter customSignals: A dictionary mapping string keys to custom
90-
/// signals to be set for the app instance.
91-
///
92-
/// When a new key is provided, a new key-value pair is added to the custom signals.
93-
/// If an existing key is provided with a new value, the corresponding signal is updated.
94-
/// If the value for a key is `nil`, the signal associated with that key is removed.
95-
func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws {
96-
return try await withCheckedThrowingContinuation { continuation in
97-
let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() }
98-
self.__setCustomSignals(customSignals) { error in
99-
if let error {
100-
continuation.resume(throwing: error)
101-
} else {
102-
continuation.resume()
103-
}
104-
}
105-
}
106-
}
107-
}

FirebaseRemoteConfig/SwiftNew/ConfigConstants.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ enum ConfigConstants {
2525

2626
/// Remote Config Error Domain.
2727
static let remoteConfigErrorDomain = "com.google.remoteconfig.ErrorDomain"
28-
// Remote Config Realtime Error Domain
28+
/// Remote Config Realtime Error Domain
2929
static let remoteConfigUpdateErrorDomain = "com.google.remoteconfig.update.ErrorDomain"
30+
/// Error domain for custom signals errors.
31+
static let remoteConfigCustomSignalsErrorDomain =
32+
"com.google.remoteconfig.customsignals.ErrorDomain"
3033

3134
// MARK: - Fetch Response Keys
3235

FirebaseRemoteConfig/SwiftNew/ConfigSettings.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,13 @@ let RCNHTTPDefaultConnectionTimeout: TimeInterval = 60
458458
// Ignore JSON serialization error.
459459
}
460460
}
461+
if customSignals.count > 0,
462+
let jsonData = try? JSONSerialization.data(withJSONObject: customSignals),
463+
let jsonString = String(data: jsonData, encoding: .utf8) {
464+
request += ", custom_signals:\(jsonString)"
465+
// Log the keys of the custom signals sent during fetch.
466+
RCLog.debug("I-RCN000078", "Keys of custom signals during fetch: \(customSignals.keys)")
467+
}
461468
}
462469
request += "}"
463470
return request
@@ -524,6 +531,14 @@ let RCNHTTPDefaultConnectionTimeout: TimeInterval = 60
524531
}
525532
}
526533

534+
/// A dictionary to hold custom signals set by the developer.
535+
@objc public var customSignals: [String: String] {
536+
get { _userDefaultsManager.customSignals }
537+
set {
538+
_userDefaultsManager.customSignals = newValue
539+
}
540+
}
541+
527542
// MARK: - Throttling
528543

529544
/// Returns true if the last fetch is outside the minimum fetch interval supplied.

FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift

Lines changed: 164 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public class RemoteConfigSettings: NSObject, NSCopying {
6565

6666
/// Indicates whether updated data was successfully fetched.
6767
@objc(FIRRemoteConfigFetchStatus)
68-
public enum RemoteConfigFetchStatus: Int {
68+
public enum RemoteConfigFetchStatus: Int, Sendable {
6969
/// Config has never been fetched.
7070
case noFetchYet
7171
/// Config fetch succeeded.
@@ -134,6 +134,17 @@ public enum RemoteConfigUpdateError: Int, LocalizedError, CustomNSError {
134134
}
135135
}
136136

137+
/// Firebase Remote Config custom signals error.
138+
@objc(FIRRemoteConfigCustomSignalsError)
139+
public enum RemoteConfigCustomSignalsError: Int, CustomNSError {
140+
/// Unknown error.
141+
case unknown = 8101
142+
/// Invalid value type in the custom signals dictionary.
143+
case invalidValueType = 8102
144+
/// Limit exceeded for key length, value length, or number of signals.
145+
case limitExceeded = 8103
146+
}
147+
137148
/// Enumerated value that indicates the source of Remote Config data. Data can come from
138149
/// the Remote Config service, the DefaultConfig that is available when the app is first
139150
/// installed, or a static initialized value if data is not available from the service or
@@ -468,7 +479,10 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
468479
/// and avoid calling this method again.
469480
///
470481
/// - Parameter completionHandler Fetch operation callback with status and error parameters.
471-
@objc public func fetch(completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)? = nil) {
482+
@objc public func fetch(completionHandler: (
483+
@Sendable (RemoteConfigFetchStatus, Error?) -> Void
484+
)? =
485+
nil) {
472486
queue.async {
473487
self.fetch(withExpirationDuration: self.settings.minimumFetchInterval,
474488
completionHandler: completionHandler)
@@ -515,7 +529,10 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
515529
/// To stop the periodic sync, call `Installations.delete(completion:)`
516530
/// and avoid calling this method again.
517531
@objc public func fetch(withExpirationDuration expirationDuration: TimeInterval,
518-
completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)? = nil) {
532+
completionHandler: (
533+
@Sendable (RemoteConfigFetchStatus, Error?) -> Void
534+
)? =
535+
nil) {
519536
configFetch.fetchConfig(withExpirationDuration: expirationDuration,
520537
completionHandler: completionHandler)
521538
}
@@ -554,8 +571,7 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
554571
///
555572
/// - Parameter completionHandler Fetch operation callback with status and error parameters.
556573
@objc public func fetchAndActivate(completionHandler:
557-
((RemoteConfigFetchAndActivateStatus, Error?) -> Void)? =
558-
nil) {
574+
(@Sendable (RemoteConfigFetchAndActivateStatus, Error?) -> Void)? = nil) {
559575
fetch { [weak self] fetchStatus, error in
560576
guard let self else { return }
561577
// Fetch completed. We are being called on the main queue.
@@ -602,7 +618,7 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
602618
/// Applies Fetched Config data to the Active Config, causing updates to the behavior and
603619
/// appearance of the app to take effect (depending on how config data is used in the app).
604620
/// - Parameter completion Activate operation callback with changed and error parameters.
605-
@objc public func activate(completion: ((Bool, Error?) -> Void)? = nil) {
621+
@objc public func activate(completion: (@Sendable (Bool, Error?) -> Void)? = nil) {
606622
queue.async { [weak self] in
607623
guard let self else {
608624
let error = NSError(
@@ -882,8 +898,9 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
882898
/// contains a remove method, which can be used to stop receiving updates for the provided
883899
/// listener.
884900
@discardableResult
885-
@objc(addOnConfigUpdateListener:) public func addOnConfigUpdateListener(remoteConfigUpdateCompletion listener: @Sendable @escaping (RemoteConfigUpdate?,
886-
Error?)
901+
@objc(addOnConfigUpdateListener:)
902+
public func addOnConfigUpdateListener(remoteConfigUpdateCompletion listener: @Sendable @escaping (RemoteConfigUpdate?,
903+
Error?)
887904
-> Void)
888905
-> ConfigUpdateListenerRegistration {
889906
return configRealtime.addConfigUpdateListener(listener)
@@ -951,6 +968,145 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
951968
}
952969
return rolloutsAssignments
953970
}
971+
972+
let customSignalsMaxKeyLength = 250
973+
let customSignalsMaxStringValueLength = 500
974+
let customSignalsMaxCount = 100
975+
976+
// MARK: - Custom Signals
977+
978+
/// Sets custom signals for this Remote Config instance.
979+
/// - Parameter customSignals: A dictionary mapping string keys to custom
980+
/// signals to be set for the app instance.
981+
///
982+
/// When a new key is provided, a new key-value pair is added to the custom signals.
983+
/// If an existing key is provided with a new value, the corresponding signal is updated.
984+
/// If the value for a key is `nil`, the signal associated with that key is removed.
985+
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
986+
public
987+
func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws {
988+
return try await withUnsafeThrowingContinuation { continuation in
989+
let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() }
990+
self.setCustomSignalsImpl(customSignals) { error in
991+
if let error {
992+
continuation.resume(throwing: error)
993+
} else {
994+
continuation.resume()
995+
}
996+
}
997+
}
998+
}
999+
1000+
@available(swift 1000.0) // Objective-C only API
1001+
@objc(setCustomSignals:withCompletion:) public func __setCustomSignals(_ customSignals: [
1002+
String: Any
1003+
]?,
1004+
withCompletion completionHandler: (
1005+
@Sendable (Error?) -> Void
1006+
)?) {
1007+
setCustomSignalsImpl(customSignals, withCompletion: completionHandler)
1008+
}
1009+
1010+
private func setCustomSignalsImpl(_ customSignals: [String: Any]?,
1011+
withCompletion completionHandler: (
1012+
@Sendable (Error?) -> Void
1013+
)?) {
1014+
queue.async { [weak self] in
1015+
guard let self else { return }
1016+
guard let customSignals = customSignals else {
1017+
if let completionHandler {
1018+
DispatchQueue.main.async {
1019+
completionHandler(nil)
1020+
}
1021+
}
1022+
return
1023+
}
1024+
1025+
// Validate value type, and key and value length
1026+
for (key, value) in customSignals {
1027+
if !(value is NSNull || value is NSString || value is NSNumber) {
1028+
let error = NSError(
1029+
domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
1030+
code: RemoteConfigCustomSignalsError.invalidValueType.rawValue,
1031+
userInfo: [
1032+
NSLocalizedDescriptionKey: "Invalid value type. Must be NSString, NSNumber, or NSNull.",
1033+
]
1034+
)
1035+
if let completionHandler {
1036+
DispatchQueue.main.async {
1037+
completionHandler(error)
1038+
}
1039+
}
1040+
return
1041+
}
1042+
1043+
if key.count > customSignalsMaxKeyLength ||
1044+
(value is NSString && (value as! NSString).length > customSignalsMaxStringValueLength) {
1045+
if let completionHandler {
1046+
let error = NSError(
1047+
domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
1048+
code: RemoteConfigCustomSignalsError.limitExceeded.rawValue,
1049+
userInfo: [
1050+
NSLocalizedDescriptionKey:
1051+
"Custom signal keys and string values must be " +
1052+
"\(customSignalsMaxKeyLength) and " +
1053+
"\(customSignalsMaxStringValueLength) " +
1054+
"characters or less respectively.",
1055+
]
1056+
)
1057+
DispatchQueue.main.async {
1058+
completionHandler(error)
1059+
}
1060+
}
1061+
return
1062+
}
1063+
}
1064+
1065+
// Merge new signals with existing ones, overwriting existing keys.
1066+
// Also, remove entries where the new value is null.
1067+
var newCustomSignals = self.settings.customSignals
1068+
1069+
for (key, value) in customSignals {
1070+
if !(value is NSNull) {
1071+
let stringValue = value is NSNumber ? (value as! NSNumber).stringValue : value as! String
1072+
newCustomSignals[key] = stringValue
1073+
} else {
1074+
newCustomSignals.removeValue(forKey: key)
1075+
}
1076+
}
1077+
1078+
// Check the size limit.
1079+
if newCustomSignals.count > customSignalsMaxCount {
1080+
if let completionHandler {
1081+
let error = NSError(
1082+
domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
1083+
code: RemoteConfigCustomSignalsError.limitExceeded.rawValue,
1084+
userInfo: [
1085+
NSLocalizedDescriptionKey:
1086+
"Custom signals count exceeds the limit of \(customSignalsMaxCount).",
1087+
]
1088+
)
1089+
DispatchQueue.main.async {
1090+
completionHandler(error)
1091+
}
1092+
}
1093+
return
1094+
}
1095+
1096+
// Update only if there are changes.
1097+
if newCustomSignals != self.settings.customSignals {
1098+
self.settings.customSignals = newCustomSignals
1099+
}
1100+
1101+
// Log the keys of the updated custom signals using RCLog.debug
1102+
RCLog.debug("I-RCN000078",
1103+
"Keys of updated custom signals: \(newCustomSignals.keys.sorted())")
1104+
1105+
DispatchQueue.main.async {
1106+
completionHandler?(nil)
1107+
}
1108+
}
1109+
}
9541110
}
9551111

9561112
// MARK: - Rollout Notification

FirebaseRemoteConfig/SwiftNew/UserDefaultsManager.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public class UserDefaultsManager: NSObject {
4949
let kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval =
5050
"currentRealtimeThrottlingRetryInterval"
5151
let kRCNUserDefaultsKeyNameRealtimeRetryCount = "realtimeRetryCount"
52+
let kRCNUserDefaultsKeyCustomSignals = "customSignals"
5253

5354
// Delete when ObjC tests are gone.
5455
@objc public convenience init(appName: String, bundleID: String, namespace: String) {
@@ -111,6 +112,13 @@ public class UserDefaultsManager: NSObject {
111112
return "\(kRCNGroupPrefix).\(bundleIdentifier).\(kRCNGroupSuffix)"
112113
}
113114

115+
@objc public var customSignals: [String: String] {
116+
get { instanceUserDefaults[kRCNUserDefaultsKeyCustomSignals] as? [String: String] ?? [:] }
117+
set {
118+
setInstanceUserDefaultsValue(newValue, forKey: kRCNUserDefaultsKeyCustomSignals)
119+
}
120+
}
121+
114122
/// The last ETag received from the server.
115123
@objc public var lastETag: String? {
116124
get { instanceUserDefaults[kRCNUserDefaultsKeyNamelastETag] as? String }

FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1949,7 +1949,7 @@ - (void)testSetCustomSignalsMultipleTimes {
19491949
[_configInstances[i] setCustomSignals:testSignals1
19501950
withCompletion:^(NSError *_Nullable error) {
19511951
XCTAssertNil(error);
1952-
[_configInstances[i]
1952+
[self->_configInstances[i]
19531953
setCustomSignals:testSignals2
19541954
withCompletion:^(NSError *_Nullable error) {
19551955
XCTAssertNil(error);

0 commit comments

Comments
 (0)