Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
58 changes: 50 additions & 8 deletions Sources/CloudKitFeatureToggles/FeatureToggleMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@
import Foundation
import CloudKit

public enum FeatureToggleValue: Equatable {
case integer(Int)
case string(String)
}

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 {
Expand All @@ -29,18 +34,55 @@ protocol FeatureToggleMappable {

class FeatureToggleMapper: FeatureToggleMappable {
private let featureToggleNameFieldID: String
private let featureToggleIsActiveFieldID: String
private let featureToggleIntValueFieldID: String
private let featureToggleStringValueFieldID: String

init(featureToggleNameFieldID: String, featureToggleIsActiveFieldID: String) {
init(featureToggleNameFieldID: String, featureToggleIntValueFieldID: String, featureToggleStringValueFieldID: String) {
self.featureToggleNameFieldID = featureToggleNameFieldID
self.featureToggleIsActiveFieldID = featureToggleIsActiveFieldID
self.featureToggleIntValueFieldID = featureToggleIntValueFieldID
self.featureToggleStringValueFieldID = featureToggleStringValueFieldID
}

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)
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
}
}
}

public extension FeatureToggleRepresentable {
var intValue: Int {
switch value {
case .integer(let int):
int
default:
fatalError("Int value used on non-int feature type")
}
}

var stringValue: String {
switch value {
case .string(let string):
string
default:
fatalError("String value used on non-string feature type")
}
}

var boolValue: Bool {
switch value {
case .integer(let i):
i != 0
case .string(let s):
(s as NSString).boolValue
}
}
}
22 changes: 19 additions & 3 deletions Sources/CloudKitFeatureToggles/FeatureToggleRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
4 changes: 2 additions & 2 deletions Sources/CloudKitFeatureToggles/FeatureToggleSubscriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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", 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, featureToggleIsActiveFieldID: featureToggleIsActiveFieldID)
self.toggleMapper = toggleMapper ?? FeatureToggleMapper(featureToggleNameFieldID: featureToggleNameFieldID, featureToggleIntValueFieldID: featureToggleIntValueFieldID, featureToggleStringValueFieldID: featureToggleStringValueFieldID)
self.featureToggleRecordID = featureToggleRecordID
self.defaults = defaults
self.notificationCenter = notificationCenter
Expand Down
32 changes: 13 additions & 19 deletions Tests/CloudKitFeatureTogglesTests/FeatureToggleMapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class FeatureToggleMapperTests: XCTestCase {
var subject: FeatureToggleMappable!

override func setUp() {
subject = FeatureToggleMapper(featureToggleNameFieldID: "featureName", featureToggleIsActiveFieldID: "isActive")
subject = FeatureToggleMapper(featureToggleNameFieldID: "name", featureToggleIntValueFieldID: "intValue", featureToggleStringValueFieldID: "stringValue")
}

func testMapInvalidInput() {
Expand All @@ -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["intValue"] = 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["intValue"] = 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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 = [
Expand Down
Loading