diff --git a/Example/FlagsmithClient.xcodeproj/project.pbxproj b/Example/FlagsmithClient.xcodeproj/project.pbxproj index 7b26991..0669203 100644 --- a/Example/FlagsmithClient.xcodeproj/project.pbxproj +++ b/Example/FlagsmithClient.xcodeproj/project.pbxproj @@ -27,7 +27,7 @@ DAED1E8E268DBF9100F91DBC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; DAED1E90268DBF9100F91DBC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; DAED1E91268DBF9100F91DBC /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - DAED1E92268DBF9100F91DBC /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + DAED1E92268DBF9100F91DBC /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; tabWidth = 2; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ diff --git a/Example/FlagsmithClient/AppDelegate.swift b/Example/FlagsmithClient/AppDelegate.swift index 04297f3..a98b03b 100644 --- a/Example/FlagsmithClient/AppDelegate.swift +++ b/Example/FlagsmithClient/AppDelegate.swift @@ -18,7 +18,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. Flagsmith.shared.apiKey = "" + + // set default flags + Flagsmith.shared.defaultFlags = [Flag(featureName: "feature_a", enabled: false), + Flag(featureName: "font_size", intValue:12, enabled: true), + Flag(featureName: "my_name", stringValue:"Testing", enabled: true)] + // set cache on / off (defaults to off) + Flagsmith.shared.cacheConfig.useCache = true + + // set custom cache to use (defaults to shared URLCache) + //Flagsmith.shared.cacheConfig.cache = + + // set skip API on / off (defaults to off) + Flagsmith.shared.cacheConfig.skipAPI = false + + // set cache TTL in seconds (defaults to 0, i.e. infinite) + Flagsmith.shared.cacheConfig.cacheTTL = 90 + // set analytics on or off Flagsmith.shared.enableAnalytics = true @@ -26,10 +43,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Flagsmith.shared.analyticsFlushPeriod = 10 Flagsmith.shared.getFeatureFlags() { (result) in - print(result) + print(result) } Flagsmith.shared.hasFeatureFlag(withID: "freeze_delinquent_accounts") { (result) in - print(result) + print(result) } //Flagsmith.shared.setTrait(Trait(key: "", value: ""), forIdentity: "") { (result) in print(result) } //Flagsmith.shared.getIdentity("") { (result) in print(result) } diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index 40cddcb..1e52f60 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 83D44370DA79D1E1C2C20ABD98D13475 /* FlagsmithClient-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 2EF53626DB8E67CE608BFEEB08DF2A5C /* FlagsmithClient-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; B7C44FEAEB3099494B79EA35B9F71DE0 /* FlagsmithClient-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = C7A4CB83310920D7A2F76B1F4F715BA5 /* FlagsmithClient-dummy.m */; }; C5344C505516395035EAB8B9FC111FE2 /* Pods-FlagsmithClient_Example-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F66D3EFFD8615AE149DCEEE155C049F /* Pods-FlagsmithClient_Example-dummy.m */; }; + DA2D2B182A1BBFF200CD98D9 /* Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2D2B172A1BBFF200CD98D9 /* Traits.swift */; }; + DAB856122A4455B2000520DC /* CachedURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB856112A4455B2000520DC /* CachedURLResponse.swift */; }; E119200D27E4E5CE00820B87 /* FlagsmithError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E119200827E4E5CE00820B87 /* FlagsmithError.swift */; }; E119200E27E4E5CE00820B87 /* FlagsmithAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = E119200A27E4E5CE00820B87 /* FlagsmithAnalytics.swift */; }; E119200F27E4E5CE00820B87 /* APIManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E119200B27E4E5CE00820B87 /* APIManager.swift */; }; @@ -44,14 +46,14 @@ 23348535C2523FAC4E5FD55FF5CA18AA /* FlagsmithClient.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = FlagsmithClient.release.xcconfig; sourceTree = ""; }; 250DE57229233B0BAD273A076F108A0E /* Pods-FlagsmithClient_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-FlagsmithClient_Example.release.xcconfig"; sourceTree = ""; }; 287971AEF04B8BFE50BA6B4E9CB5A76E /* Flagsmith+Concurrency.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Flagsmith+Concurrency.swift"; path = "FlagsmithClient/Classes/Flagsmith+Concurrency.swift"; sourceTree = ""; }; - 2DDEFBF90EE0DCCBEE1F3D989919BC4A /* Flag.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Flag.swift; path = FlagsmithClient/Classes/Flag.swift; sourceTree = ""; }; + 2DDEFBF90EE0DCCBEE1F3D989919BC4A /* Flag.swift */ = {isa = PBXFileReference; includeInIndex = 1; indentWidth = 2; lastKnownFileType = sourcecode.swift; name = Flag.swift; path = FlagsmithClient/Classes/Flag.swift; sourceTree = ""; tabWidth = 2; }; 2EF53626DB8E67CE608BFEEB08DF2A5C /* FlagsmithClient-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FlagsmithClient-umbrella.h"; sourceTree = ""; }; 368DFAE2AAAB80524EFAFD71A2C92F84 /* Pods-FlagsmithClient_Example-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-FlagsmithClient_Example-acknowledgements.markdown"; sourceTree = ""; }; 3B040CFA25391975C1615BFB481B68C9 /* Pods-FlagsmithClient_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-FlagsmithClient_Example.debug.xcconfig"; sourceTree = ""; }; 68DDF334F75630BAA85571DF47D87C89 /* FlagsmithClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FlagsmithClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 69E74567D7DE2313D6054FA08CFC5940 /* FlagsmithClient-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FlagsmithClient-prefix.pch"; sourceTree = ""; }; 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; - 757CB2DED75CA13C202204D5B88B7B9A /* Flagsmith.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Flagsmith.swift; path = FlagsmithClient/Classes/Flagsmith.swift; sourceTree = ""; }; + 757CB2DED75CA13C202204D5B88B7B9A /* Flagsmith.swift */ = {isa = PBXFileReference; includeInIndex = 1; indentWidth = 2; lastKnownFileType = sourcecode.swift; name = Flagsmith.swift; path = FlagsmithClient/Classes/Flagsmith.swift; sourceTree = ""; tabWidth = 2; }; 772F9BEA66F23FD7DB187ACBEB72BA93 /* UnknownTypeValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UnknownTypeValue.swift; path = FlagsmithClient/Classes/UnknownTypeValue.swift; sourceTree = ""; }; 7CB5FDC929BB21CB23FFC8143AF6322F /* FlagsmithClient.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; path = FlagsmithClient.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 81A815D8A0C28062CD4A8224C6883D5D /* Pods-FlagsmithClient_Example.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-FlagsmithClient_Example.modulemap"; sourceTree = ""; }; @@ -66,9 +68,11 @@ C1CEBBBF5F9F986FB033B56D22422A4A /* Trait.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Trait.swift; path = FlagsmithClient/Classes/Trait.swift; sourceTree = ""; }; C55271DDAE55BE2BA1AFA657FA24971A /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; C7A4CB83310920D7A2F76B1F4F715BA5 /* FlagsmithClient-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "FlagsmithClient-dummy.m"; sourceTree = ""; }; + DA2D2B172A1BBFF200CD98D9 /* Traits.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Traits.swift; path = FlagsmithClient/Classes/Traits.swift; sourceTree = ""; }; + DAB856112A4455B2000520DC /* CachedURLResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedURLResponse.swift; sourceTree = ""; }; E119200827E4E5CE00820B87 /* FlagsmithError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FlagsmithError.swift; path = FlagsmithClient/Classes/FlagsmithError.swift; sourceTree = ""; }; E119200A27E4E5CE00820B87 /* FlagsmithAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlagsmithAnalytics.swift; sourceTree = ""; }; - E119200B27E4E5CE00820B87 /* APIManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIManager.swift; sourceTree = ""; }; + E119200B27E4E5CE00820B87 /* APIManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = APIManager.swift; sourceTree = ""; tabWidth = 2; }; E119200C27E4E5CE00820B87 /* Router.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; E190202D27E24E7600C5EE2C /* TypedValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TypedValue.swift; path = FlagsmithClient/Classes/TypedValue.swift; sourceTree = ""; }; E28010F1C58E656FC37588C8A00FEE38 /* Pods-FlagsmithClient_Example-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-FlagsmithClient_Example-Info.plist"; sourceTree = ""; }; @@ -122,6 +126,7 @@ 1A94E973B95A4D1E5D88FED450500C7B /* FlagsmithClient */ = { isa = PBXGroup; children = ( + DA2D2B172A1BBFF200CD98D9 /* Traits.swift */, A8A7BD53895AF16384B78CB7116DEBD7 /* Feature.swift */, 2DDEFBF90EE0DCCBEE1F3D989919BC4A /* Flag.swift */, 757CB2DED75CA13C202204D5B88B7B9A /* Flagsmith.swift */, @@ -212,6 +217,7 @@ E119200927E4E5CE00820B87 /* Internal */ = { isa = PBXGroup; children = ( + DAB856112A4455B2000520DC /* CachedURLResponse.swift */, E119200A27E4E5CE00820B87 /* FlagsmithAnalytics.swift */, E119200B27E4E5CE00820B87 /* APIManager.swift */, E119200C27E4E5CE00820B87 /* Router.swift */, @@ -338,6 +344,8 @@ 4A510C0AE712CD782D25F2276904F827 /* Flag.swift in Sources */, E190202E27E24E7600C5EE2C /* TypedValue.swift in Sources */, 1845D603B597C651F0EE6080EF459DBB /* Flagsmith.swift in Sources */, + DAB856122A4455B2000520DC /* CachedURLResponse.swift in Sources */, + DA2D2B182A1BBFF200CD98D9 /* Traits.swift in Sources */, 2C55F74AE43F34E4F3952F5B86AADBF5 /* Flagsmith+Concurrency.swift in Sources */, E119200F27E4E5CE00820B87 /* APIManager.swift in Sources */, E119201027E4E5CE00820B87 /* Router.swift in Sources */, diff --git a/FlagsmithClient.podspec b/FlagsmithClient.podspec index 70b5599..a517467 100644 --- a/FlagsmithClient.podspec +++ b/FlagsmithClient.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'FlagsmithClient' - s.version = '3.2.1' + s.version = '3.3.0' s.summary = 'iOS Client written in Swift for Flagsmith. Ship features with confidence using feature flags and remote config.' s.homepage = 'https://github.com/Flagsmith/flagsmith-ios-client' s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/FlagsmithClient/Classes/Feature.swift b/FlagsmithClient/Classes/Feature.swift index 6657a81..1c0ce75 100644 --- a/FlagsmithClient/Classes/Feature.swift +++ b/FlagsmithClient/Classes/Feature.swift @@ -10,7 +10,7 @@ import Foundation /** A Feature represents a flag or remote configuration value on the server. */ -public struct Feature: Decodable { +public struct Feature: Codable { enum CodingKeys: String, CodingKey { case name case type @@ -21,4 +21,17 @@ public struct Feature: Decodable { public let name: String public let type: String? public let description: String? + + init(name: String, type: String?, description: String?) { + self.name = name + self.type = type + self.description = description + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.name, forKey: .name) + try container.encodeIfPresent(self.type, forKey: .type) + try container.encodeIfPresent(self.description, forKey: .description) + } } diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index 7c61c26..1929beb 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -10,7 +10,7 @@ import Foundation /** A Flag represents a feature flag on the server. */ -public struct Flag: Decodable { +public struct Flag: Codable { enum CodingKeys: String, CodingKey { case feature case value = "feature_state_value" @@ -20,4 +20,37 @@ public struct Flag: Decodable { public let feature: Feature public let value: TypedValue public let enabled: Bool + + public init(featureName:String, boolValue: Bool, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { + self.init(featureName: featureName, value: TypedValue.bool(boolValue), enabled: enabled, featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName:String, floatValue: Float, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { + self.init(featureName: featureName, value: TypedValue.float(floatValue), enabled: enabled, featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName:String, intValue: Int, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { + self.init(featureName: featureName, value: TypedValue.int(intValue), enabled: enabled, featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName:String, stringValue: String, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { + self.init(featureName: featureName, value: TypedValue.string(stringValue), enabled: enabled, featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName:String, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { + self.init(featureName: featureName, value: TypedValue.null, enabled: enabled, featureType: featureType, featureDescription: featureDescription) + } + + public init(featureName:String, value: TypedValue, enabled: Bool, featureType:String? = nil, featureDescription:String? = nil) { + self.feature = Feature(name: featureName, type: featureType, description: featureDescription) + self.value = value + self.enabled = enabled + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.feature, forKey: .feature) + try container.encode(self.value, forKey: .value) + try container.encode(self.enabled, forKey: .enabled) + } } diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index c8565dc..7c03c4e 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -6,6 +6,9 @@ // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif /// Manage feature flags and remote config across multiple projects, /// environments and organisations. @@ -42,8 +45,15 @@ public class Flagsmith { set { analytics.flushPeriod = newValue } get { analytics.flushPeriod } } + + /// Default flags to fall back on if an API call fails + public var defaultFlags: [Flag] = [] + + /// Configuration class for the cache settings + public var cacheConfig:CacheConfig = CacheConfig() - private init() {} + private init() { + } /// Get all feature flags (flags and remote config) optionally for a specific identity /// @@ -52,18 +62,34 @@ public class Flagsmith { /// - completion: Closure with Result which contains array of Flag objects in case of success or Error in case of failure public func getFeatureFlags(forIdentity identity: String? = nil, completion: @escaping (Result<[Flag], Error>) -> Void) { + if let identity = identity { getIdentity(identity) { (result) in switch result { - case .success(let identity): - completion(.success(identity.flags)) + case .success(let thisIdentity): + completion(.success(thisIdentity.flags)) case .failure(let error): - completion(.failure(error)) + if self.defaultFlags.isEmpty { + completion(.failure(error)) + } + else { + completion(.success(self.defaultFlags)) + } } } } else { apiManager.request(.getFlags) { (result: Result<[Flag], Error>) in - completion(result) + switch result { + case .success(let flags): + completion(.success(flags)) + case .failure(let error): + if self.defaultFlags.isEmpty { + completion(.failure(error)) + } + else { + completion(.success(self.defaultFlags)) + } + } } } } @@ -84,7 +110,12 @@ public class Flagsmith { let hasFlag = flags.contains(where: {$0.feature.name == id && $0.enabled}) completion(.success(hasFlag)) case .failure(let error): - completion(.failure(error)) + if self.defaultFlags.contains(where: {$0.feature.name == id && $0.enabled}) { + completion(.success(true)) + } + else { + completion(.failure(error)) + } } } } @@ -103,10 +134,15 @@ public class Flagsmith { getFeatureFlags(forIdentity: identity) { (result) in switch result { case .success(let flags): - let value = flags.first(where: {$0.feature.name == id})?.value - completion(.success(value?.stringValue)) + let flag = flags.first(where: {$0.feature.name == id}) + completion(.success(flag?.value.stringValue)) case .failure(let error): - completion(.failure(error)) + if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { + completion(.success(flag.value.stringValue)) + } + else { + completion(.failure(error)) + } } } } @@ -124,10 +160,15 @@ public class Flagsmith { getFeatureFlags(forIdentity: identity) { (result) in switch result { case .success(let flags): - let value = flags.first(where: {$0.feature.name == id})?.value - completion(.success(value)) + var flag = flags.first(where: {$0.feature.name == id}) + completion(.success(flag?.value)) case .failure(let error): - completion(.failure(error)) + if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { + completion(.success(flag.value)) + } + else { + completion(.failure(error)) + } } } } @@ -215,4 +256,25 @@ public class Flagsmith { completion(result) } } + + /// Return a flag for a flag ID from the default flags. + private func getFlagUsingDefaults(withID id: String, forIdentity identity: String? = nil) -> Flag? { + return self.defaultFlags.first(where: {$0.feature.name == id}) + } +} + +public class CacheConfig { + + /// Cache to use when enabled, defaults to the shared app cache + public var cache: URLCache = URLCache.shared + + /// Use cached flags as a fallback? + public var useCache: Bool = false + + /// TTL for the cache in seconds, default of 0 means infinite + public var cacheTTL: Double = 0 + + /// Skip API if there is a cache available + public var skipAPI: Bool = false + } diff --git a/FlagsmithClient/Classes/Internal/APIManager.swift b/FlagsmithClient/Classes/Internal/APIManager.swift index a47bafd..2d3626a 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -11,20 +11,62 @@ import FoundationNetworking #endif /// Handles interaction with a **Flagsmith** api. -class APIManager { +class APIManager : NSObject, URLSessionDataDelegate { - private let session: URLSession + private var session: URLSession! /// Base `URL` used for requests. var baseURL = URL(string: "https://edge.api.flagsmith.com/api/v1/")! /// API Key unique to an organization. var apiKey: String? + + // store the completion handlers and accumulated data for each task + private var tasksToCompletionHandlers:[URLSessionDataTask:(Result) -> Void] = [:] + private var tasksToData:[URLSessionDataTask:NSMutableData] = [:] - init() { + override init() { + super.init() let configuration = URLSessionConfiguration.default - self.session = URLSession(configuration: configuration) + self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let dataTask = task as? URLSessionDataTask { + if let completion = tasksToCompletionHandlers[dataTask] { + if let error = error { + completion(.failure(FlagsmithError.unhandled(error))) + } + else { + let data = tasksToData[dataTask] ?? NSMutableData() + completion(.success(data as Data)) + } + } + tasksToCompletionHandlers[dataTask] = nil + tasksToData[dataTask] = nil + } + } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) { + + // intercept and modify the cache settings for the response + if Flagsmith.shared.cacheConfig.useCache { + let newResponse = proposedResponse.response(withExpirationDuration: Int(Flagsmith.shared.cacheConfig.cacheTTL)) + completionHandler(newResponse) + } else { + completionHandler(proposedResponse) + } } + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + let existingData = tasksToData[dataTask] ?? NSMutableData() + existingData.append(data) + tasksToData[dataTask] = existingData + } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + completionHandler(.allow) + } + /// Base request method that handles creating a `URLRequest` and processing /// the `URLSession` response. /// @@ -37,7 +79,7 @@ class APIManager { return } - let request: URLRequest + var request: URLRequest do { request = try router.request(baseUrl: baseURL, apiKey: apiKey) } catch { @@ -45,23 +87,20 @@ class APIManager { return } - session.dataTask(with: request) { data, response, error in - guard error == nil else { - completion(.failure(FlagsmithError.unhandled(error!))) - return - } - - let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 - guard (200...299).contains(statusCode) else { - completion(.failure(FlagsmithError.statusCode(statusCode))) - return + // set the cache policy based on Flagsmith settings + request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData + session.configuration.urlCache = Flagsmith.shared.cacheConfig.cache + if Flagsmith.shared.cacheConfig.useCache { + request.cachePolicy = .useProtocolCachePolicy + if Flagsmith.shared.cacheConfig.skipAPI { + request.cachePolicy = .returnCacheDataElseLoad } - - // The documentation indicates the data should be provided - // since error was found to be nil at this point. Either way - // the 'Decodable' variation will handle any invalid `Data`. - completion(.success(data ?? Data())) - }.resume() + } + + // we must use the delegate form here, not the completion handler, to be able to modify the cache + let task = session.dataTask(with: request) + tasksToCompletionHandlers[task] = completion + task.resume() } /// Requests a api route and only relays success or failure of the action. diff --git a/FlagsmithClient/Classes/Internal/CachedURLResponse.swift b/FlagsmithClient/Classes/Internal/CachedURLResponse.swift new file mode 100644 index 0000000..730fce2 --- /dev/null +++ b/FlagsmithClient/Classes/Internal/CachedURLResponse.swift @@ -0,0 +1,29 @@ +// +// File.swift +// CachedURLResponse +// +// Created by Daniel Wichett on 21/06/2023. +// + +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +extension CachedURLResponse { + func response(withExpirationDuration duration: Int) -> CachedURLResponse { + var cachedResponse = self + if let httpResponse = cachedResponse.response as? HTTPURLResponse, var headers = httpResponse.allHeaderFields as? [String : String], let url = httpResponse.url{ + + //set to 1 year (the max allowed) if the value is 0 + headers["Cache-Control"] = "max-age=\(duration == 0 ? 31536000 : duration)" + headers.removeValue(forKey: "Expires") + headers.removeValue(forKey: "s-maxage") + + if let newResponse = HTTPURLResponse(url: url, statusCode: httpResponse.statusCode, httpVersion: "HTTP/1.1", headerFields: headers) { + cachedResponse = CachedURLResponse(response: newResponse, data: cachedResponse.data, userInfo: headers, storagePolicy: cachedResponse.storagePolicy) + } + } + return cachedResponse + } +}