diff --git a/RealFlags/Sources/RealFlags/Classes/Flag/Flag.swift b/RealFlags/Sources/RealFlags/Classes/Flag/Flag.swift index 3be5577..70289c1 100644 --- a/RealFlags/Sources/RealFlags/Classes/Flag/Flag.swift +++ b/RealFlags/Sources/RealFlags/Classes/Flag/Flag.swift @@ -10,7 +10,7 @@ // Licensed under MIT License. // -import UIKit +import Foundation /// This a wrapper which represent a single Feature Flag. /// The type that you wrap with `@Flag` must conform to `FlagProtocol`. diff --git a/RealFlags/Sources/RealFlags/Classes/Flag/FlagProtocol/EncodedFlagValue.swift b/RealFlags/Sources/RealFlags/Classes/Flag/FlagProtocol/EncodedFlagValue.swift index 3d8872a..eafff8f 100644 --- a/RealFlags/Sources/RealFlags/Classes/Flag/FlagProtocol/EncodedFlagValue.swift +++ b/RealFlags/Sources/RealFlags/Classes/Flag/FlagProtocol/EncodedFlagValue.swift @@ -66,7 +66,7 @@ public enum EncodedFlagValue: Equatable { } } - /// Trnsform boxed data in a valid `NSObject` you can store. + /// Transform boxed data in a valid `NSObject` you can store. /// /// - Returns: NSObject internal func nsObject() -> NSObject { diff --git a/RealFlags/Sources/RealFlags/Classes/Providers/FlagsProvider.swift b/RealFlags/Sources/RealFlags/Classes/Providers/FlagsProvider.swift index 91bf332..58e6ae6 100644 --- a/RealFlags/Sources/RealFlags/Classes/Providers/FlagsProvider.swift +++ b/RealFlags/Sources/RealFlags/Classes/Providers/FlagsProvider.swift @@ -10,7 +10,7 @@ // Licensed under MIT License. // -#if !os(Linux) +#if canImport(Combine) import Combine #endif @@ -47,14 +47,12 @@ public protocol FlagsProvider { /// Reset the value for keypath; it will remove the value from the record of the flag provider. func resetValueForFlag(key: FlagKeyPath) throws - #if !os(Linux) + #if canImport(Combine) // Apple platform also support Combine framework to provide realtime notification of new events to any subscriber. // By default it does nothing (take a look to the default implementation in extension below). - /// You can use this value to receive updates when keys did updated in sources. - /// - /// - Parameter keys: updated keys; may be `nil` if your provider does not support this kind of granularity. - func didUpdateValuesForKeys(_ keys: Set?) -> AnyPublisher, Never>? + /// You can use this value to receive updates when a flag's values changes inside the current `FlagsProvider`. + func didUpdateValueForKey() -> AnyPublisher<(FlagKeyPath, (any FlagProtocol)?), Never>? #endif @@ -62,13 +60,13 @@ public protocol FlagsProvider { // MARK: - FlagsProvider (Default Publisher Behaviour) -#if !os(Linux) +#if canImport(Combine) /// Make support for real-time flag updates optional by providing a default nil implementation /// public extension FlagsProvider { - func didUpdateValuesForKeys(_ keys: Set?) -> AnyPublisher, Never>? { + func didUpdateValueForKey() -> AnyPublisher<(FlagKeyPath, (any FlagProtocol)?), Never>? { nil } diff --git a/RealFlags/Sources/RealFlags/Classes/Providers/Providers/LocalProvider.swift b/RealFlags/Sources/RealFlags/Classes/Providers/Providers/LocalProvider.swift index 551ce29..92067ed 100644 --- a/RealFlags/Sources/RealFlags/Classes/Providers/Providers/LocalProvider.swift +++ b/RealFlags/Sources/RealFlags/Classes/Providers/Providers/LocalProvider.swift @@ -11,6 +11,9 @@ // import Foundation +#if canImport(Combine) +import Combine +#endif /// LocalProvider is a local source for feature flags. You can use this object for persistent local data /// or ephemeral storage. @@ -39,6 +42,11 @@ public class LocalProvider: FlagsProvider, Identifiable { /// Storage data. internal var storage: [String: Any] + #if canImport(Combine) + /// `PassthroughSubject` that emits when a flag's values changes inside this `LocalProvider` + private let didUpdateValueForKeyPublisher: PassthroughSubject<(FlagKeyPath, (any FlagProtocol)?), Never> = .init() + #endif + // MARK: - Initialization /// Initialize a new ephemeral storage which will be never saved locally. @@ -82,6 +90,11 @@ public class LocalProvider: FlagsProvider, Identifiable { BentoDict.setValueForDictionary(&storage, value: encodedValue, keyPath: key) try saveToDisk() + + #if canImport(Combine) + didUpdateValueForKeyPublisher.send((key, value)) + #endif + return true } @@ -119,4 +132,10 @@ public class LocalProvider: FlagsProvider, Identifiable { storage.removeAll() } + #if canImport(Combine) + public func didUpdateValueForKey() -> AnyPublisher<(FlagKeyPath, (any FlagProtocol)?), Never>? { + didUpdateValueForKeyPublisher + .eraseToAnyPublisher() + } + #endif } diff --git a/Tests/RealFlagsTests/LocalProviderTests.swift b/Tests/RealFlagsTests/LocalProviderTests.swift index 555f6ed..032baff 100644 --- a/Tests/RealFlagsTests/LocalProviderTests.swift +++ b/Tests/RealFlagsTests/LocalProviderTests.swift @@ -12,11 +12,13 @@ import Foundation import XCTest +import Combine @testable import RealFlags class LocalProviderTests: XCTestCase { fileprivate var loader: FlagsLoader! + private var cancellables: Set! fileprivate lazy var localProviderFileURL: URL = { let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] @@ -29,6 +31,7 @@ class LocalProviderTests: XCTestCase { override func setUp() { super.setUp() loader = FlagsLoader(LPFlagsCollection.self, providers: [localProvider]) + cancellables = [] } override func tearDown() { @@ -77,6 +80,46 @@ class LocalProviderTests: XCTestCase { loader.nested.$flagInt.setDefault(2) } } + + func testDidUpdateValueForKeyPublisher() throws { + // Setup subscriber + let expectation = expectation(description: "didUpdateValueForKeyPublisher") + var didUpdateValueForKeyReceivedValues: [(FlagKeyPath, (any FlagProtocol)?)] = [] + + localProvider.didUpdateValueForKey()? + .collect(5) + .sink(receiveValue: { values in + didUpdateValueForKeyReceivedValues = values + expectation.fulfill() + }) + .store(in: &cancellables) + + // Change values + let _ = try localProvider.setValue(true, forFlag: loader.$flagBool.keyPath) + let _ = try localProvider.setValue(false, forFlag: loader.$flagBool.keyPath) + let _ = try localProvider.setValue(10, forFlag: loader.nested.$flagInt.keyPath) + let _ = try localProvider.setValue("Test", forFlag: loader.nested.$flagString.keyPath) + let _ = try localProvider.setValue(Optional.none, forFlag: loader.nested.$flagString.keyPath) + + wait(for: [expectation], timeout: 10) + + // Assertions on received values + XCTAssertEqual(5, didUpdateValueForKeyReceivedValues.count) + let keyPaths: [FlagKeyPath] = didUpdateValueForKeyReceivedValues.map { $0.0 } + let flags: [(any FlagProtocol)?] = didUpdateValueForKeyReceivedValues.map { $0.1 } + + XCTAssertEqual(loader.$flagBool.keyPath, keyPaths[0]) + XCTAssertEqual(loader.$flagBool.keyPath, keyPaths[1]) + XCTAssertEqual(loader.nested.$flagInt.keyPath, keyPaths[2]) + XCTAssertEqual(loader.nested.$flagString.keyPath, keyPaths[3]) + XCTAssertEqual(loader.nested.$flagString.keyPath, keyPaths[4]) + + XCTAssertEqual(true.encoded(), flags[0]?.encoded()) + XCTAssertEqual(false.encoded(), flags[1]?.encoded()) + XCTAssertEqual(10.encoded(), flags[2]?.encoded()) + XCTAssertEqual("Test".encoded(), flags[3]?.encoded()) + XCTAssertEqual(nil, flags[4]?.encoded()) + } }