From 8ca7b2223a532dd5f82b20d27acc6f62c8e64555 Mon Sep 17 00:00:00 2001 From: Sim Saens Date: Sun, 4 May 2025 15:39:14 +0930 Subject: [PATCH 1/7] Updates library to support integer and string value types --- README.md | 4 +- .../FeatureToggleMapper.swift | 39 +++++++++++++---- .../FeatureToggleRepository.swift | 22 ++++++++-- .../FeatureToggleSubscriptor.swift | 4 +- .../FeatureToggleMapperTests.swift | 32 ++++++-------- .../FeatureToggleRepositoryTests.swift | 32 +++++++------- .../FeatureToggleSubscriptorTests.swift | 42 +++++++++---------- 7 files changed, 104 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index ae4581f..674c01e 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ And don't forget to add the dependency to your target(s). | Field | Type | | --- | --- | -| `featureName` | `String` | -| `isActive` | `Int64` | +| `name` | `String` | +| `value` | `Any` | For each feature toggle you want to support in your application later add a new record in your CloudKit *public database*. diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift index 404537e..63f1097 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift @@ -8,19 +8,33 @@ import Foundation import CloudKit +public enum FeatureToggleValue: Equatable { + case integer(Int) + case string(String) +} + +public extension FeatureToggleValue { + var boolValue: Bool { + switch self { + case .integer(let i): return i != 0 + case .string(let s): return (s as NSString).boolValue + } + } +} + public protocol FeatureToggleRepresentable { var identifier: String { get } - var isActive: Bool { get } + var value: FeatureToggleValue { get } } public protocol FeatureToggleIdentifiable { var identifier: String { get } - var fallbackValue: Bool { get } + var fallbackValue: FeatureToggleValue { get } } public struct FeatureToggle: FeatureToggleRepresentable, Equatable { public let identifier: String - public let isActive: Bool + public let value: FeatureToggleValue } protocol FeatureToggleMappable { @@ -29,18 +43,27 @@ protocol FeatureToggleMappable { class FeatureToggleMapper: FeatureToggleMappable { private let featureToggleNameFieldID: String - private let featureToggleIsActiveFieldID: String + private let featureToggleValueFieldID: String - init(featureToggleNameFieldID: String, featureToggleIsActiveFieldID: String) { + init(featureToggleNameFieldID: String, featureToggleValueFieldID: String) { self.featureToggleNameFieldID = featureToggleNameFieldID - self.featureToggleIsActiveFieldID = featureToggleIsActiveFieldID + self.featureToggleValueFieldID = featureToggleValueFieldID } func map(record: CKRecord) -> FeatureToggle? { - guard let isActive = record[featureToggleIsActiveFieldID] as? Int64, let featureName = record[featureToggleNameFieldID] as? String else { + guard let featureName = record[featureToggleNameFieldID] as? String else { return nil } - return FeatureToggle(identifier: featureName, isActive: NSNumber(value: isActive).boolValue) + let value = record[featureToggleValueFieldID] + + switch value { + case let value as String: + return FeatureToggle(identifier: featureName, value: .string(value)) + case let value as Int: + return FeatureToggle(identifier: featureName, value: .integer(value)) + default: + return nil + } } } diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift index ffb4163..563d46e 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift @@ -26,12 +26,28 @@ public class FeatureToggleUserDefaultsRepository { extension FeatureToggleUserDefaultsRepository: FeatureToggleRepository { public func retrieve(identifiable: FeatureToggleIdentifiable) -> FeatureToggleRepresentable { - let isActive = defaults.value(forKey: identifiable.identifier) as? Bool + let storedValue = defaults.value(forKey: identifiable.identifier) + let value: FeatureToggleValue - return FeatureToggle(identifier: identifiable.identifier, isActive: isActive ?? identifiable.fallbackValue) + switch storedValue { + + case let int as Int: + value = .integer(int) + case let string as String: + value = .string(string) + default: + value = identifiable.fallbackValue + } + + return FeatureToggle(identifier: identifiable.identifier, value: value) } public func save(featureToggle: FeatureToggleRepresentable) { - defaults.set(featureToggle.isActive, forKey: featureToggle.identifier) + switch featureToggle.value { + case .integer(let intValue): + defaults.set(intValue, forKey: featureToggle.identifier) + case .string(let stringValue): + defaults.set(stringValue, forKey: featureToggle.identifier) + } } } diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index c82fbc2..ee19302 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -22,9 +22,9 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { let subscriptionID = "cloudkit-recordType-FeatureToggle" let database: CloudKitDatabaseConformable - init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), toggleMapper: FeatureToggleMappable? = nil, featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "featureName", featureToggleIsActiveFieldID: String = "isActive", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, notificationCenter: NotificationCenter = .default, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { + init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), toggleMapper: FeatureToggleMappable? = nil, featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "name", featureToggleValueFieldID: String = "value", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, notificationCenter: NotificationCenter = .default, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { self.toggleRepository = toggleRepository - self.toggleMapper = toggleMapper ?? FeatureToggleMapper(featureToggleNameFieldID: featureToggleNameFieldID, featureToggleIsActiveFieldID: featureToggleIsActiveFieldID) + self.toggleMapper = toggleMapper ?? FeatureToggleMapper(featureToggleNameFieldID: featureToggleNameFieldID, featureToggleValueFieldID: featureToggleValueFieldID) self.featureToggleRecordID = featureToggleRecordID self.defaults = defaults self.notificationCenter = notificationCenter diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift index a174253..593fc73 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift @@ -14,7 +14,7 @@ class FeatureToggleMapperTests: XCTestCase { var subject: FeatureToggleMappable! override func setUp() { - subject = FeatureToggleMapper(featureToggleNameFieldID: "featureName", featureToggleIsActiveFieldID: "isActive") + subject = FeatureToggleMapper(featureToggleNameFieldID: "name", featureToggleValueFieldID: "value") } func testMapInvalidInput() { @@ -32,53 +32,47 @@ class FeatureToggleMapperTests: XCTestCase { let wrongIsActiveField = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier3")) wrongIsActiveField["bla"] = true - wrongIsActiveField["featureName"] = 1283765 + wrongIsActiveField["name"] = 1283765 XCTAssertNil(subject.map(record: wrongIsActiveField)) let wrongFeatureNameField = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier4")) - wrongFeatureNameField["isActive"] = true + wrongFeatureNameField["value"] = true wrongFeatureNameField["muh"] = 1283765 XCTAssertNil(subject.map(record: wrongFeatureNameField)) - let wrongIsActiveType = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier5")) - wrongIsActiveType["isActive"] = "true" - wrongIsActiveType["featureName"] = "1283765" - - XCTAssertNil(subject.map(record: wrongIsActiveType)) - let wrongFeatureNameType = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier6")) - wrongFeatureNameType["isActive"] = true - wrongFeatureNameType["featureName"] = 1283765 + wrongFeatureNameType["value"] = true + wrongFeatureNameType["name"] = 1283765 XCTAssertNil(subject.map(record: wrongFeatureNameType)) } func testMap() { let expectedIdentifier = "1283765" - let expectedIsActive = true + let expectedValue: FeatureToggleValue = .integer(1) let record = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier")) - record["isActive"] = expectedIsActive - record["featureName"] = expectedIdentifier + record["value"] = true + record["name"] = expectedIdentifier let result = subject.map(record: record) XCTAssertNotNil(result) - XCTAssertEqual(result, FeatureToggle(identifier: expectedIdentifier, isActive: expectedIsActive)) + XCTAssertEqual(result, FeatureToggle(identifier: expectedIdentifier, value: expectedValue)) } func testMap2() { let expectedIdentifier = "akjshgdjaskd(/(/&%$ยง" - let expectedIsActive = false + let expectedValue: FeatureToggleValue = .integer(0) let record = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier")) - record["isActive"] = expectedIsActive - record["featureName"] = expectedIdentifier + record["value"] = false + record["name"] = expectedIdentifier let result = subject.map(record: record) XCTAssertNotNil(result) - XCTAssertEqual(result, FeatureToggle(identifier: expectedIdentifier, isActive: expectedIsActive)) + XCTAssertEqual(result, FeatureToggle(identifier: expectedIdentifier, value: expectedValue)) } static var allTests = [ diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleRepositoryTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleRepositoryTests.swift index ae61e77..29715fa 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleRepositoryTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleRepositoryTests.swift @@ -15,12 +15,12 @@ class FeatureToggleRepositoryTests: XCTestCase { return self.rawValue } - var fallbackValue: Bool { + var fallbackValue: FeatureToggleValue { switch self { case .feature1: - return false + return .integer(0) case .feature2: - return true + return .integer(1) } } @@ -52,25 +52,25 @@ class FeatureToggleRepositoryTests: XCTestCase { } func testRetrieveBeforeSave() { - XCTAssertEqual(subject.retrieve(identifiable: TestToggle.feature1).isActive, TestToggle.feature1.fallbackValue) - XCTAssertEqual(subject.retrieve(identifiable: TestToggle.feature2).isActive, TestToggle.feature2.fallbackValue) + XCTAssertEqual(subject.retrieve(identifiable: TestToggle.feature1).value, TestToggle.feature1.fallbackValue) + XCTAssertEqual(subject.retrieve(identifiable: TestToggle.feature2).value, TestToggle.feature2.fallbackValue) - XCTAssertFalse(subject.retrieve(identifiable: TestToggle.feature1).isActive) - subject.save(featureToggle: FeatureToggle(identifier: TestToggle.feature1.rawValue, isActive: true)) - XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature1).isActive) + XCTAssertFalse(subject.retrieve(identifiable: TestToggle.feature1).value == .integer(1)) + subject.save(featureToggle: FeatureToggle(identifier: TestToggle.feature1.rawValue, value: .integer(1))) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature1).value == .integer(1)) } func testSaveAndRetrieve() { - XCTAssertFalse(subject.retrieve(identifiable: TestToggle.feature1).isActive) - XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature2).isActive) + XCTAssertFalse(subject.retrieve(identifiable: TestToggle.feature1).value == .integer(1)) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature2).value == .integer(1)) - subject.save(featureToggle: FeatureToggle(identifier: TestToggle.feature1.rawValue, isActive: true)) - XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature1).isActive) - XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature2).isActive) + subject.save(featureToggle: FeatureToggle(identifier: TestToggle.feature1.rawValue, value: .integer(1))) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature1).value == .integer(1)) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature2).value == .integer(1)) - subject.save(featureToggle: FeatureToggle(identifier: TestToggle.feature2.rawValue, isActive: false)) - XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature1).isActive) - XCTAssertFalse(subject.retrieve(identifiable: TestToggle.feature2).isActive) + subject.save(featureToggle: FeatureToggle(identifier: TestToggle.feature2.rawValue, value: .integer(0))) + XCTAssertTrue(subject.retrieve(identifiable: TestToggle.feature1).value == .integer(1)) + XCTAssertFalse(subject.retrieve(identifiable: TestToggle.feature2).value == .integer(1)) } static var allTests = [ diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift index e00f77b..01217d5 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -25,7 +25,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { cloudKitDatabase = MockCloudKitDatabaseConformable() repository = MockToggleRepository() - subject = FeatureToggleSubscriptor(toggleRepository: repository, featureToggleRecordID: "TestFeatureStatus", featureToggleNameFieldID: "toggleName", featureToggleIsActiveFieldID: "isActive", defaults: defaults, cloudKitDatabaseConformable: cloudKitDatabase) + subject = FeatureToggleSubscriptor(toggleRepository: repository, featureToggleRecordID: "TestFeatureStatus", featureToggleNameFieldID: "name", featureToggleValueFieldID: "value", defaults: defaults, cloudKitDatabaseConformable: cloudKitDatabase) } override func tearDown() { @@ -38,8 +38,8 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertNil(cloudKitDatabase.recordType) XCTAssertEqual(repository.toggles.count, 0) - cloudKitDatabase.recordFetched["isActive"] = 1 - cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.fetchAll() @@ -49,10 +49,10 @@ class FeatureToggleSubscriptorTests: XCTestCase { return } XCTAssertEqual(toggle.identifier, "Toggle1") - XCTAssertTrue(toggle.isActive) + XCTAssertEqual(toggle.value, .integer(1)) - cloudKitDatabase.recordFetched["isActive"] = 0 - cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + cloudKitDatabase.recordFetched["value"] = 0 + cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.fetchAll() @@ -63,7 +63,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { return } XCTAssertEqual(toggle2.identifier, "Toggle1") - XCTAssertFalse(toggle2.isActive) + XCTAssertEqual(toggle2.value, .integer(0)) } func testFetchAllError() { @@ -72,8 +72,8 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertNil(cloudKitDatabase.recordType) XCTAssertEqual(repository.toggles.count, 0) - cloudKitDatabase.recordFetched["isActive"] = 1 - cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.fetchAll() @@ -89,8 +89,8 @@ class FeatureToggleSubscriptorTests: XCTestCase { return toggles.count == 1 } - cloudKitDatabase.recordFetched["isActive"] = 1 - cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.fetchAll() wait(for: [expectation], timeout: 0.1) @@ -149,8 +149,8 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertEqual(cloudKitDatabase.addCalledCount, 0) XCTAssertEqual(repository.toggles.count, 0) - cloudKitDatabase.recordFetched["isActive"] = 1 - cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.handleNotification() @@ -162,10 +162,10 @@ class FeatureToggleSubscriptorTests: XCTestCase { return } XCTAssertEqual(toggle.identifier, "Toggle1") - XCTAssertTrue(toggle.isActive) + XCTAssertEqual(toggle.value, .integer(1)) - cloudKitDatabase.recordFetched["isActive"] = 0 - cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + cloudKitDatabase.recordFetched["value"] = 0 + cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.handleNotification() @@ -178,7 +178,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { return } XCTAssertEqual(toggle2.identifier, "Toggle1") - XCTAssertFalse(toggle2.isActive) + XCTAssertEqual(toggle2.value, .integer(0)) } func testHandleNotificationSendNotification() { @@ -189,8 +189,8 @@ class FeatureToggleSubscriptorTests: XCTestCase { return toggles.count == 1 } - cloudKitDatabase.recordFetched["isActive"] = 1 - cloudKitDatabase.recordFetched["toggleName"] = "Toggle1" + cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.handleNotification() wait(for: [expectation], timeout: 0.1) @@ -239,13 +239,13 @@ class MockToggleRepository: FeatureToggleRepository { func retrieve(identifiable: FeatureToggleIdentifiable) -> FeatureToggleRepresentable { toggles.first { (representable) -> Bool in representable.identifier == identifiable.identifier - } ?? MockToggleRepresentable(identifier: identifiable.identifier, isActive: identifiable.fallbackValue) + } ?? MockToggleRepresentable(identifier: identifiable.identifier, value: identifiable.fallbackValue) } } struct MockToggleRepresentable: FeatureToggleRepresentable { var identifier: String - var isActive: Bool + var value: FeatureToggleValue } class MockCloudKitDatabaseConformable: CloudKitDatabaseConformable { From 250271f592a90d0148ee939b9db13d1a507ac4c6 Mon Sep 17 00:00:00 2001 From: Sim Saens Date: Sun, 4 May 2025 17:05:56 +0930 Subject: [PATCH 2/7] Updated CKRecord mapper to use int/stringValue fields --- .../FeatureToggleMapper.swift | 23 +++++++++---------- .../FeatureToggleSubscriptor.swift | 4 ++-- .../FeatureToggleMapperTests.swift | 6 ++--- .../FeatureToggleSubscriptorTests.swift | 16 ++++++------- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift index 63f1097..d73ee2d 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift @@ -43,11 +43,13 @@ protocol FeatureToggleMappable { class FeatureToggleMapper: FeatureToggleMappable { private let featureToggleNameFieldID: String - private let featureToggleValueFieldID: String + private let featureToggleIntValueFieldID: String + private let featureToggleStringValueFieldID: String - init(featureToggleNameFieldID: String, featureToggleValueFieldID: String) { + init(featureToggleNameFieldID: String, featureToggleIntValueFieldID: String, featureToggleStringValueFieldID: String) { self.featureToggleNameFieldID = featureToggleNameFieldID - self.featureToggleValueFieldID = featureToggleValueFieldID + self.featureToggleIntValueFieldID = featureToggleIntValueFieldID + self.featureToggleStringValueFieldID = featureToggleStringValueFieldID } func map(record: CKRecord) -> FeatureToggle? { @@ -55,15 +57,12 @@ class FeatureToggleMapper: FeatureToggleMappable { return nil } - let value = record[featureToggleValueFieldID] - - switch value { - case let value as String: - return FeatureToggle(identifier: featureName, value: .string(value)) - case let value as Int: - return FeatureToggle(identifier: featureName, value: .integer(value)) - default: - return nil + return if let value = record[featureToggleIntValueFieldID] as? Int { + FeatureToggle(identifier: featureName, value: .integer(value)) + } else if let value = record[featureToggleStringValueFieldID] as? String { + FeatureToggle(identifier: featureName, value: .string(value)) + } else { + nil } } } diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift index ee19302..58b68c7 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift @@ -22,9 +22,9 @@ class FeatureToggleSubscriptor: CloudKitSubscriptionProtocol { let subscriptionID = "cloudkit-recordType-FeatureToggle" let database: CloudKitDatabaseConformable - init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), toggleMapper: FeatureToggleMappable? = nil, featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "name", featureToggleValueFieldID: String = "value", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, notificationCenter: NotificationCenter = .default, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { + init(toggleRepository: FeatureToggleRepository = FeatureToggleUserDefaultsRepository(), toggleMapper: FeatureToggleMappable? = nil, featureToggleRecordID: String = "FeatureStatus", featureToggleNameFieldID: String = "name", featureToggleIntValueFieldID: String = "intValue", featureToggleStringValueFieldID: String = "stringValue", defaults: UserDefaults = UserDefaults(suiteName: FeatureToggleSubscriptor.defaultsSuiteName) ?? .standard, notificationCenter: NotificationCenter = .default, cloudKitDatabaseConformable: CloudKitDatabaseConformable = CKContainer.default().publicCloudDatabase) { self.toggleRepository = toggleRepository - self.toggleMapper = toggleMapper ?? FeatureToggleMapper(featureToggleNameFieldID: featureToggleNameFieldID, featureToggleValueFieldID: featureToggleValueFieldID) + self.toggleMapper = toggleMapper ?? FeatureToggleMapper(featureToggleNameFieldID: featureToggleNameFieldID, featureToggleIntValueFieldID: featureToggleIntValueFieldID, featureToggleStringValueFieldID: featureToggleStringValueFieldID) self.featureToggleRecordID = featureToggleRecordID self.defaults = defaults self.notificationCenter = notificationCenter diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift index 593fc73..9e2bee6 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift @@ -14,7 +14,7 @@ class FeatureToggleMapperTests: XCTestCase { var subject: FeatureToggleMappable! override func setUp() { - subject = FeatureToggleMapper(featureToggleNameFieldID: "name", featureToggleValueFieldID: "value") + subject = FeatureToggleMapper(featureToggleNameFieldID: "name", featureToggleIntValueFieldID: "intValue", featureToggleStringValueFieldID: "stringValue") } func testMapInvalidInput() { @@ -54,7 +54,7 @@ class FeatureToggleMapperTests: XCTestCase { let expectedValue: FeatureToggleValue = .integer(1) let record = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier")) - record["value"] = true + record["intValue"] = true record["name"] = expectedIdentifier let result = subject.map(record: record) @@ -67,7 +67,7 @@ class FeatureToggleMapperTests: XCTestCase { let expectedValue: FeatureToggleValue = .integer(0) let record = CKRecord(recordType: "FeatureStatus", recordID: CKRecord.ID(recordName: "identifier")) - record["value"] = false + record["intValue"] = false record["name"] = expectedIdentifier let result = subject.map(record: record) diff --git a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift index 01217d5..5111ad3 100644 --- a/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift +++ b/Tests/CloudKitFeatureTogglesTests/FeatureToggleSubscriptorTests.swift @@ -25,7 +25,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { cloudKitDatabase = MockCloudKitDatabaseConformable() repository = MockToggleRepository() - subject = FeatureToggleSubscriptor(toggleRepository: repository, featureToggleRecordID: "TestFeatureStatus", featureToggleNameFieldID: "name", featureToggleValueFieldID: "value", defaults: defaults, cloudKitDatabaseConformable: cloudKitDatabase) + subject = FeatureToggleSubscriptor(toggleRepository: repository, featureToggleRecordID: "TestFeatureStatus", featureToggleNameFieldID: "name", featureToggleIntValueFieldID: "intValue", featureToggleStringValueFieldID: "stringValue", defaults: defaults, cloudKitDatabaseConformable: cloudKitDatabase) } override func tearDown() { @@ -38,7 +38,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertNil(cloudKitDatabase.recordType) XCTAssertEqual(repository.toggles.count, 0) - cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["intValue"] = 1 cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.fetchAll() @@ -51,7 +51,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertEqual(toggle.identifier, "Toggle1") XCTAssertEqual(toggle.value, .integer(1)) - cloudKitDatabase.recordFetched["value"] = 0 + cloudKitDatabase.recordFetched["intValue"] = 0 cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.fetchAll() @@ -72,7 +72,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertNil(cloudKitDatabase.recordType) XCTAssertEqual(repository.toggles.count, 0) - cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["intValue"] = 1 cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.fetchAll() @@ -89,7 +89,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { return toggles.count == 1 } - cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["intValue"] = 1 cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.fetchAll() @@ -149,7 +149,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertEqual(cloudKitDatabase.addCalledCount, 0) XCTAssertEqual(repository.toggles.count, 0) - cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["intValue"] = 1 cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.handleNotification() @@ -164,7 +164,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { XCTAssertEqual(toggle.identifier, "Toggle1") XCTAssertEqual(toggle.value, .integer(1)) - cloudKitDatabase.recordFetched["value"] = 0 + cloudKitDatabase.recordFetched["intValue"] = 0 cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.handleNotification() @@ -189,7 +189,7 @@ class FeatureToggleSubscriptorTests: XCTestCase { return toggles.count == 1 } - cloudKitDatabase.recordFetched["value"] = 1 + cloudKitDatabase.recordFetched["intValue"] = 1 cloudKitDatabase.recordFetched["name"] = "Toggle1" subject.handleNotification() From d4f3c141cead108503771b2d86bd41e301afb7f6 Mon Sep 17 00:00:00 2001 From: Sim Saens Date: Sun, 4 May 2025 19:13:37 +0930 Subject: [PATCH 3/7] Use async remote notification delegate method --- .../FeatureToggleApplicationService.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift index acc66c7..fa48d3d 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleApplicationService.swift @@ -61,12 +61,17 @@ extension FeatureToggleApplicationService: UIApplicationDelegate { return true } - public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo), let subscriptionID = notification.subscriptionID else { - return + public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) async -> UIBackgroundFetchResult { + guard let notification = CKNotification(fromRemoteNotificationDictionary: userInfo), + let subscriptionID = notification.subscriptionID else { + return .noData + } + + return await withCheckedContinuation { continuation in + handleRemoteNotification(subscriptionID: subscriptionID) { result in + continuation.resume(returning: result) + } } - - handleRemoteNotification(subscriptionID: subscriptionID, completionHandler: completionHandler) } } #endif From 6e16a4ca6f1da53d12ffa76f916c11fcd6d56cb8 Mon Sep 17 00:00:00 2001 From: Sim Saens Date: Sun, 4 May 2025 19:14:50 +0930 Subject: [PATCH 4/7] Updated supported versions --- Package.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index da614fe..21f44bc 100644 --- a/Package.swift +++ b/Package.swift @@ -6,10 +6,10 @@ import PackageDescription let package = Package( name: "CloudKitFeatureToggles", platforms: [ - .iOS(SupportedPlatform.IOSVersion.v10), - .macOS(SupportedPlatform.MacOSVersion.v10_12), - .tvOS(SupportedPlatform.TVOSVersion.v9), - .watchOS(SupportedPlatform.WatchOSVersion.v3) + .iOS(.v13), + .macOS(.v10_12), + .tvOS(.v9), + .watchOS(.v3) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. From e623205efa959a8179136f0192aa64158d9dcea4 Mon Sep 17 00:00:00 2001 From: Sim Saens Date: Sun, 4 May 2025 22:07:25 +0930 Subject: [PATCH 5/7] Adds convenience extension to representable protocol --- .../FeatureToggleMapper.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift index d73ee2d..4b38913 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift @@ -66,3 +66,23 @@ class FeatureToggleMapper: FeatureToggleMappable { } } } + +public extension FeatureToggleRepresentable { + var intValue: Int? { + switch value { + case .integer(let int): + int + default: + nil + } + } + + var stringValue: String? { + switch value { + case .string(let string): + string + default: + nil + } + } +} From 559c56bdb11fe96ffbc23992bfa17bdb5ea8dd96 Mon Sep 17 00:00:00 2001 From: Sim Saens Date: Sun, 4 May 2025 22:13:50 +0930 Subject: [PATCH 6/7] Moves boolValue helper --- .../FeatureToggleMapper.swift | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift index 4b38913..06dabfd 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift @@ -13,15 +13,6 @@ public enum FeatureToggleValue: Equatable { case string(String) } -public extension FeatureToggleValue { - var boolValue: Bool { - switch self { - case .integer(let i): return i != 0 - case .string(let s): return (s as NSString).boolValue - } - } -} - public protocol FeatureToggleRepresentable { var identifier: String { get } var value: FeatureToggleValue { get } @@ -85,4 +76,11 @@ public extension FeatureToggleRepresentable { nil } } + + var boolValue: Bool { + switch value { + case .integer(let i): return i != 0 + case .string(let s): return (s as NSString).boolValue + } + } } From 958a800c722ddb239627ec126eb1def40896875f Mon Sep 17 00:00:00 2001 From: Sim Saens Date: Sun, 4 May 2025 22:15:37 +0930 Subject: [PATCH 7/7] Made string and int value non-optional --- .../FeatureToggleMapper.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift index 06dabfd..626fd31 100644 --- a/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift +++ b/Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift @@ -59,28 +59,30 @@ class FeatureToggleMapper: FeatureToggleMappable { } public extension FeatureToggleRepresentable { - var intValue: Int? { + var intValue: Int { switch value { case .integer(let int): int default: - nil + fatalError("Int value used on non-int feature type") } } - var stringValue: String? { + var stringValue: String { switch value { case .string(let string): string default: - nil + fatalError("String value used on non-string feature type") } } var boolValue: Bool { switch value { - case .integer(let i): return i != 0 - case .string(let s): return (s as NSString).boolValue + case .integer(let i): + i != 0 + case .string(let s): + (s as NSString).boolValue } } }