From b79283e924f64fefe5b7fa5b7129336699332c9a Mon Sep 17 00:00:00 2001 From: 3a4oT Date: Tue, 12 Mar 2024 20:01:49 +0200 Subject: [PATCH 01/10] - Bumped `swift-tools` to swift 5.9. - Enabled `StrictConcurrency=complete` mode in order to be prepared for upcoming Swift 6. Implemented `Sendable` support for Flagsmith client and friends. - Enabled `ExistentialAny` feature in order to be prepared for upcoming Swift 6. Code adjustments - adjusted tests for `StrictConcurrency=complete` mode . --- FlagsmithClient/Classes/Feature.swift | 2 +- FlagsmithClient/Classes/Flag.swift | 2 +- FlagsmithClient/Classes/Flagsmith.swift | 496 +++++++++--------- FlagsmithClient/Classes/FlagsmithError.swift | 6 +- .../Classes/Internal/APIManager.swift | 281 +++++----- .../Classes/Internal/FlagsmithAnalytics.swift | 226 ++++---- FlagsmithClient/Classes/Trait.swift | 4 +- FlagsmithClient/Classes/TypedValue.swift | 6 +- .../Classes/UnknownTypeValue.swift | 2 +- FlagsmithClient/Tests/APIManagerTests.swift | 49 +- Package.swift | 10 +- 11 files changed, 589 insertions(+), 495 deletions(-) diff --git a/FlagsmithClient/Classes/Feature.swift b/FlagsmithClient/Classes/Feature.swift index 446bb4f..adf3473 100644 --- a/FlagsmithClient/Classes/Feature.swift +++ b/FlagsmithClient/Classes/Feature.swift @@ -28,7 +28,7 @@ public struct Feature: Codable, Sendable { self.description = description } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.name, forKey: .name) try container.encodeIfPresent(self.type, forKey: .type) diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index b9a9ebc..1bd63f5 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -47,7 +47,7 @@ public struct Flag: Codable, Sendable { self.enabled = enabled } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.feature, forKey: .feature) try container.encode(self.value, forKey: .value) diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index 495fff6..777d489 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -12,269 +12,291 @@ import FoundationNetworking /// Manage feature flags and remote config across multiple projects, /// environments and organisations. -public class Flagsmith { - /// Shared singleton client object - public static let shared = Flagsmith() - private let apiManager = APIManager() - private lazy var analytics = FlagsmithAnalytics(apiManager: apiManager) - - /// Base URL - /// - /// The default implementation uses: `https://edge.api.flagsmith.com/api/v1`. - public var baseURL: URL { - set { apiManager.baseURL = newValue } - get { apiManager.baseURL } - } - - /// API Key unique to your organization. - /// - /// This value must be provided before any request can succeed. - public var apiKey: String? { - set { apiManager.apiKey = newValue } - get { apiManager.apiKey } - } +public final class Flagsmith: @unchecked Sendable { + /// Shared singleton client object + public static let shared: Flagsmith = Flagsmith() + private let apiManager: APIManager + private let analytics: FlagsmithAnalytics - /// Is flag analytics enabled? - public var enableAnalytics: Bool { - set { analytics.enableAnalytics = newValue } - get { analytics.enableAnalytics } - } + /// Base URL + /// + /// The default implementation uses: `https://edge.api.flagsmith.com/api/v1`. + public var baseURL: URL { + set { apiManager.baseURL = newValue } + get { apiManager.baseURL } + } + + /// API Key unique to your organization. + /// + /// This value must be provided before any request can succeed. + public var apiKey: String? { + set { apiManager.apiKey = newValue } + get { apiManager.apiKey } + } + + /// Is flag analytics enabled? + public var enableAnalytics: Bool { + set { analytics.enableAnalytics = newValue } + get { analytics.enableAnalytics } + } - /// How often to send the flag analytics, in seconds - public var analyticsFlushPeriod: Int { - 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() + /// How often to send the flag analytics, in seconds + public var analyticsFlushPeriod: Int { + set { analytics.flushPeriod = newValue } + get { analytics.flushPeriod } + } - private init() { - } - - /// Get all feature flags (flags and remote config) optionally for a specific identity - /// - /// - Parameters: - /// - identity: ID of the user (optional) - /// - 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 thisIdentity): - completion(.success(thisIdentity.flags)) - case .failure(let error): - if self.defaultFlags.isEmpty { - completion(.failure(error)) - } - else { - completion(.success(self.defaultFlags)) - } + /// Default flags to fall back on if an API call fails + private var _defaultFlags: [Flag] = [] + public var defaultFlags: [Flag] { + get { + apiManager.propertiesSerialAccessQueue.sync { _defaultFlags } } - } - } else { - apiManager.request(.getFlags) { (result: Result<[Flag], Error>) in - 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)) - } + set { + apiManager.propertiesSerialAccessQueue.sync { + _defaultFlags = newValue + } } - } } - } - - /// Check feature exists and is enabled optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - completion: Closure with Result which contains Bool in case of success or Error in case of failure - public func hasFeatureFlag(withID id: String, - forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) { - analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in - switch result { - case .success(let flags): - let hasFlag = flags.contains(where: {$0.feature.name == id && $0.enabled}) - completion(.success(hasFlag)) - case .failure(let error): - if self.defaultFlags.contains(where: {$0.feature.name == id && $0.enabled}) { - completion(.success(true)) + + /// Configuration class for the cache settings + private var _cacheConfig: CacheConfig = CacheConfig() + public var cacheConfig: CacheConfig { + get { + apiManager.propertiesSerialAccessQueue.sync { _cacheConfig } } - else { - completion(.failure(error)) + set { + apiManager.propertiesSerialAccessQueue.sync { + _cacheConfig = newValue + } } - } } - } - - /// Get remote config value optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - completion: Closure with Result which String in case of success or Error in case of failure - @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") - public func getFeatureValue(withID id: String, - forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) { - analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in - switch result { - case .success(let flags): - let flag = flags.first(where: {$0.feature.name == id}) - completion(.success(flag?.value.stringValue)) - case .failure(let error): - if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { - completion(.success(flag.value.stringValue)) + + private init() { + apiManager = APIManager() + analytics = FlagsmithAnalytics(apiManager: apiManager) + } + + /// Get all feature flags (flags and remote config) optionally for a specific identity + /// + /// - Parameters: + /// - identity: ID of the user (optional) + /// - 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: @Sendable @escaping (Result<[Flag], any Error>) -> Void) { + + if let identity = identity { + getIdentity(identity) { (result) in + switch result { + case .success(let thisIdentity): + completion(.success(thisIdentity.flags)) + case .failure(let error): + if self.defaultFlags.isEmpty { + completion(.failure(error)) + } + else { + completion(.success(self.defaultFlags)) + } + } + } + } else { + apiManager.request(.getFlags) { (result: Result<[Flag], Error>) in + 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)) + } + } + } } - else { - completion(.failure(error)) + } + + /// Check feature exists and is enabled optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - completion: Closure with Result which contains Bool in case of success or Error in case of failure + public func hasFeatureFlag(withID id: String, + forIdentity identity: String? = nil, + completion: @Sendable @escaping (Result) -> Void) { + analytics.trackEvent(flagName: id) + getFeatureFlags(forIdentity: identity) { (result) in + switch result { + case .success(let flags): + let hasFlag = flags.contains(where: {$0.feature.name == id && $0.enabled}) + completion(.success(hasFlag)) + case .failure(let error): + if self.defaultFlags.contains(where: {$0.feature.name == id && $0.enabled}) { + completion(.success(true)) + } + else { + completion(.failure(error)) + } + } } - } } - } - - /// Get remote config value optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - completion: Closure with Result of `TypedValue` in case of success or `Error` in case of failure - public func getValueForFeature(withID id: String, - forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) { - analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in - switch result { - case .success(let flags): - let flag = flags.first(where: {$0.feature.name == id}) - completion(.success(flag?.value)) - case .failure(let error): - if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { - completion(.success(flag.value)) + + /// Get remote config value optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - completion: Closure with Result which String in case of success or Error in case of failure + @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") + public func getFeatureValue(withID id: String, + forIdentity identity: String? = nil, + completion: @Sendable @escaping (Result) -> Void) { + analytics.trackEvent(flagName: id) + getFeatureFlags(forIdentity: identity) { (result) in + switch result { + case .success(let flags): + let flag = flags.first(where: {$0.feature.name == id}) + completion(.success(flag?.value.stringValue)) + case .failure(let error): + if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { + completion(.success(flag.value.stringValue)) + } + else { + completion(.failure(error)) + } + } } - else { - completion(.failure(error)) + } + + /// Get remote config value optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - completion: Closure with Result of `TypedValue` in case of success or `Error` in case of failure + public func getValueForFeature(withID id: String, + forIdentity identity: String? = nil, + completion: @Sendable @escaping (Result) -> Void) { + analytics.trackEvent(flagName: id) + getFeatureFlags(forIdentity: identity) { (result) in + switch result { + case .success(let flags): + let flag = flags.first(where: {$0.feature.name == id}) + completion(.success(flag?.value)) + case .failure(let error): + if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { + completion(.success(flag.value)) + } + else { + completion(.failure(error)) + } + } } - } } - } - - /// Get all user traits for provided identity. Optionally filter results with a list of keys - /// - /// - Parameters: - /// - ids: IDs of the trait (optional) - /// - identity: ID of the user - /// - completion: Closure with Result which contains array of Trait objects in case of success or Error in case of failure - public func getTraits(withIDS ids: [String]? = nil, - forIdentity identity: String, - completion: @escaping (Result<[Trait], Error>) -> Void) { - getIdentity(identity) { (result) in - switch result { - case .success(let identity): - if let ids = ids { - let traits = identity.traits.filter({ids.contains($0.key)}) - completion(.success(traits)) - } else { - completion(.success(identity.traits)) + + /// Get all user traits for provided identity. Optionally filter results with a list of keys + /// + /// - Parameters: + /// - ids: IDs of the trait (optional) + /// - identity: ID of the user + /// - completion: Closure with Result which contains array of Trait objects in case of success or Error in case of failure + public func getTraits(withIDS ids: [String]? = nil, + forIdentity identity: String, + completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) { + getIdentity(identity) { (result) in + switch result { + case .success(let identity): + if let ids = ids { + let traits = identity.traits.filter({ids.contains($0.key)}) + completion(.success(traits)) + } else { + completion(.success(identity.traits)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + /// Get user trait for provided identity and trait key + /// + /// - Parameters: + /// - id: ID of the trait + /// - identity: ID of the user + /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure + public func getTrait(withID id: String, + forIdentity identity: String, + completion: @Sendable @escaping (Result) -> Void) { + getIdentity(identity) { (result) in + switch result { + case .success(let identity): + let trait = identity.traits.first(where: {$0.key == id}) + completion(.success(trait)) + case .failure(let error): + completion(.failure(error)) + } } - case .failure(let error): - completion(.failure(error)) - } } - } - - /// Get user trait for provided identity and trait key - /// - /// - Parameters: - /// - id: ID of the trait - /// - identity: ID of the user - /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure - public func getTrait(withID id: String, - forIdentity identity: String, - completion: @escaping (Result) -> Void) { - getIdentity(identity) { (result) in - switch result { - case .success(let identity): - let trait = identity.traits.first(where: {$0.key == id}) - completion(.success(trait)) - case .failure(let error): - completion(.failure(error)) - } + + /// Set user trait for provided identity + /// + /// - Parameters: + /// - trait: Trait to be created or updated + /// - identity: ID of the user + /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure + public func setTrait(_ trait: Trait, + forIdentity identity: String, + completion: @Sendable @escaping (Result) -> Void) { + apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in + completion(result) + } } - } - - /// Set user trait for provided identity - /// - /// - Parameters: - /// - trait: Trait to be created or updated - /// - identity: ID of the user - /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure - public func setTrait(_ trait: Trait, - forIdentity identity: String, - completion: @escaping (Result) -> Void) { - apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in - completion(result) + + /// Set user traits in bulk for provided identity + /// + /// - Parameters: + /// - traits: Traits to be created or updated + /// - identity: ID of the user + /// - completion: Closure with Result which contains Traits in case of success or Error in case of failure + public func setTraits(_ traits: [Trait], + forIdentity identity: String, + completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) { + apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in + completion(result.map(\.traits)) + } } - } - /// Set user traits in bulk for provided identity - /// - /// - Parameters: - /// - traits: Traits to be created or updated - /// - identity: ID of the user - /// - completion: Closure with Result which contains Traits in case of success or Error in case of failure - public func setTraits(_ traits: [Trait], - forIdentity identity: String, - completion: @escaping (Result<[Trait], Error>) -> Void) { - apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in - completion(result.map(\.traits)) + /// Get both feature flags and user traits for the provided identity + /// + /// - Parameters: + /// - identity: ID of the user + /// - completion: Closure with Result which contains Identity in case of success or Error in case of failure + public func getIdentity(_ identity: String, + completion: @Sendable @escaping (Result) -> Void) { + apiManager.request(.getIdentity(identity: identity)) { (result: Result) in + completion(result) + } } - } - - /// Get both feature flags and user traits for the provided identity - /// - /// - Parameters: - /// - identity: ID of the user - /// - completion: Closure with Result which contains Identity in case of success or Error in case of failure - public func getIdentity(_ identity: String, - completion: @escaping (Result) -> Void) { - apiManager.request(.getIdentity(identity: identity)) { (result: Result) in - 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}) } - } - - /// 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 { +public final class CacheConfig { + + /// Cache to use when enabled, defaults to the shared app cache + public var cache: URLCache = URLCache.shared - /// 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 - /// 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 - /// 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 - /// Skip API if there is a cache available - public var skipAPI: Bool = false - } diff --git a/FlagsmithClient/Classes/FlagsmithError.swift b/FlagsmithClient/Classes/FlagsmithError.swift index fda9675..672d4b2 100644 --- a/FlagsmithClient/Classes/FlagsmithError.swift +++ b/FlagsmithClient/Classes/FlagsmithError.swift @@ -20,8 +20,8 @@ public enum FlagsmithError: LocalizedError, Sendable { /// API Response could not be decoded. case decoding(DecodingError) /// Unknown or unhandled error was encountered. - case unhandled(Error) - + case unhandled(any Error) + public var errorDescription: String? { switch self { case .apiKey: @@ -46,7 +46,7 @@ public enum FlagsmithError: LocalizedError, Sendable { /// * as `EncodingError`: `.encoding()` error will be created. /// * as `DecodingError`: `.decoding()` error will be created. /// * default: `.unhandled()` error will be created. - internal init(_ error: Error) { + internal init(_ error: any Error) { switch error { case let flagsmithError as FlagsmithError: self = flagsmithError diff --git a/FlagsmithClient/Classes/Internal/APIManager.swift b/FlagsmithClient/Classes/Internal/APIManager.swift index b13b898..d5bb506 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -11,142 +11,173 @@ import FoundationNetworking #endif /// Handles interaction with a **Flagsmith** api. -class APIManager : NSObject, URLSessionDataDelegate { - - 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:[Int:(Result) -> Void] = [:] - private var tasksToData:[Int:Data] = [:] - private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue") - - override init() { - super.init() - let configuration = URLSessionConfiguration.default - self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) - } - - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - serialAccessQueue.sync { - if let dataTask = task as? URLSessionDataTask { - if let completion = tasksToCompletionHandlers[dataTask.taskIdentifier] { - if let error = error { - DispatchQueue.main.async { completion(.failure(FlagsmithError.unhandled(error))) } - } - else { - let data = tasksToData[dataTask.taskIdentifier] ?? Data() - DispatchQueue.main.async { completion(.success(data)) } - } +final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { + + private var _session: URLSession! + private var session: URLSession { + get { + propertiesSerialAccessQueue.sync { _session } + } + set { + propertiesSerialAccessQueue.sync(flags: .barrier) { + _session = newValue + } } - tasksToCompletionHandlers[dataTask.taskIdentifier] = nil - tasksToData[dataTask.taskIdentifier] = nil - } - } - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, - completionHandler: @escaping (CachedURLResponse?) -> Void) { - serialAccessQueue.sync { - // intercept and modify the cache settings for the response - if Flagsmith.shared.cacheConfig.useCache { - let newResponse = proposedResponse.response(withExpirationDuration: Int(Flagsmith.shared.cacheConfig.cacheTTL)) - DispatchQueue.main.async { completionHandler(newResponse) } - } else { - DispatchQueue.main.async { completionHandler(proposedResponse) } - } } - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - serialAccessQueue.sync { - var existingData = tasksToData[dataTask.taskIdentifier] ?? Data() - existingData.append(data) - tasksToData[dataTask.taskIdentifier] = existingData + + /// Base `URL` used for requests. + private var _baseURL = URL(string: "https://edge.api.flagsmith.com/api/v1/")! + var baseURL: URL { + get { + propertiesSerialAccessQueue.sync { _baseURL } + } + set { + propertiesSerialAccessQueue.sync { + _baseURL = newValue + } + } } - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - completionHandler(.allow) + /// API Key unique to an organization. + private var _apiKey: String? + var apiKey: String? { + get { + propertiesSerialAccessQueue.sync { _apiKey } + } + set { + propertiesSerialAccessQueue.sync { + _apiKey = newValue + } + } } - - /// Base request method that handles creating a `URLRequest` and processing - /// the `URLSession` response. - /// - /// - parameters: - /// - router: The path and parameters that should be requested. - /// - completion: Function block executed with the result of the request. - private func request(_ router: Router, completion: @escaping (Result) -> Void) { - guard let apiKey = apiKey, !apiKey.isEmpty else { - completion(.failure(FlagsmithError.apiKey)) - return + + // store the completion handlers and accumulated data for each task + private var tasksToCompletionHandlers:[Int: @Sendable(Result) -> Void] = [:] + private var tasksToData:[Int:Data] = [:] + private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue") + let propertiesSerialAccessQueue = DispatchQueue(label: "propertiesSerialAccessQueue") + + override init() { + super.init() + let configuration = URLSessionConfiguration.default + self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) } - - var request: URLRequest - do { - request = try router.request(baseUrl: baseURL, apiKey: apiKey) - } catch { - completion(.failure(error)) - return + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { + serialAccessQueue.sync { + if let dataTask = task as? URLSessionDataTask { + if let completion = tasksToCompletionHandlers[dataTask.taskIdentifier] { + if let error = error { + DispatchQueue.main.async { completion(.failure(FlagsmithError.unhandled(error))) } + } + else { + let data = tasksToData[dataTask.taskIdentifier] ?? Data() + DispatchQueue.main.async { completion(.success(data)) } + } + } + tasksToCompletionHandlers[dataTask.taskIdentifier] = nil + tasksToData[dataTask.taskIdentifier] = nil + } + } } - - // 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 - } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, + completionHandler: @Sendable @escaping (CachedURLResponse?) -> Void) { + serialAccessQueue.sync { + // intercept and modify the cache settings for the response + if Flagsmith.shared.cacheConfig.useCache { + let newResponse = proposedResponse.response(withExpirationDuration: Int(Flagsmith.shared.cacheConfig.cacheTTL)) + DispatchQueue.main.async { completionHandler(newResponse) } + } else { + DispatchQueue.main.async { completionHandler(proposedResponse) } + } + } } - - // we must use the delegate form here, not the completion handler, to be able to modify the cache - serialAccessQueue.sync { - let task = session.dataTask(with: request) - tasksToCompletionHandlers[task.taskIdentifier] = completion - task.resume() + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + serialAccessQueue.sync { + var existingData = tasksToData[dataTask.taskIdentifier] ?? Data() + existingData.append(data) + tasksToData[dataTask.taskIdentifier] = existingData + } } - } - - /// Requests a api route and only relays success or failure of the action. - /// - /// - parameters: - /// - router: The path and parameters that should be requested. - /// - completion: Function block executed with the result of the request. - func request(_ router: Router, completion: @escaping (Result) -> Void) { - request(router) { (result: Result) in - switch result { - case .failure(let error): - completion(.failure(FlagsmithError(error))) - case .success: - completion(.success(())) - } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + completionHandler(.allow) } - } - - /// Requests a api route and attempts the decode the response. - /// - /// - parameters: - /// - router: The path and parameters that should be requested. - /// - decoder: `JSONDecoder` used to deserialize the response data. - /// - completion: Function block executed with the result of the request. - func request(_ router: Router, using decoder: JSONDecoder = JSONDecoder(), completion: @escaping (Result) -> Void) { - request(router) { (result: Result) in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let data): + + /// Base request method that handles creating a `URLRequest` and processing + /// the `URLSession` response. + /// + /// - parameters: + /// - router: The path and parameters that should be requested. + /// - completion: Function block executed with the result of the request. + private func request(_ router: Router, completion: @Sendable @escaping (Result) -> Void) { + guard let apiKey = apiKey, !apiKey.isEmpty else { + completion(.failure(FlagsmithError.apiKey)) + return + } + + var request: URLRequest do { - let value = try decoder.decode(T.self, from: data) - completion(.success(value)) + request = try router.request(baseUrl: baseURL, apiKey: apiKey) } catch { - completion(.failure(FlagsmithError(error))) + completion(.failure(error)) + 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 + } + } + + // we must use the delegate form here, not the completion handler, to be able to modify the cache + serialAccessQueue.sync { + let task = session.dataTask(with: request) + tasksToCompletionHandlers[task.taskIdentifier] = completion + task.resume() + } + } + + /// Requests a api route and only relays success or failure of the action. + /// + /// - parameters: + /// - router: The path and parameters that should be requested. + /// - completion: Function block executed with the result of the request. + func request(_ router: Router, completion: @Sendable @escaping (Result) -> Void) { + request(router) { (result: Result) in + switch result { + case .failure(let error): + completion(.failure(FlagsmithError(error))) + case .success: + completion(.success(())) + } + } + } + + /// Requests a api route and attempts the decode the response. + /// + /// - parameters: + /// - router: The path and parameters that should be requested. + /// - decoder: `JSONDecoder` used to deserialize the response data. + /// - completion: Function block executed with the result of the request. + func request(_ router: Router, using decoder: JSONDecoder = JSONDecoder(), completion: @Sendable @escaping (Result) -> Void) { + request(router) { (result: Result) in + switch result { + case .failure(let error): + completion(.failure(error)) + case .success(let data): + do { + let value = try decoder.decode(T.self, from: data) + completion(.success(value)) + } catch { + completion(.failure(FlagsmithError(error))) + } + } } - } } - } } diff --git a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift index d172b3b..1d6c3f0 100644 --- a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift +++ b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift @@ -8,102 +8,140 @@ import Foundation /// Internal analytics for the **FlagsmithClient** -class FlagsmithAnalytics { - - /// Indicates if analytics are enabled. - var enableAnalytics: Bool = true - /// How often analytics events are processed (in seconds). - var flushPeriod: Int = 10 { - didSet { - setupTimer() +final class FlagsmithAnalytics: @unchecked Sendable { + + /// Indicates if analytics are enabled. + private var _enableAnalytics: Bool = true + var enableAnalytics:Bool { + get { + apiManager.propertiesSerialAccessQueue.sync { _enableAnalytics } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _enableAnalytics = newValue + } + } + } + + private var _flushPeriod: Int = 10 + /// How often analytics events are processed (in seconds). + var flushPeriod: Int { + get { + apiManager.propertiesSerialAccessQueue.sync { _flushPeriod } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _flushPeriod = newValue + } + setupTimer() + } + } + + private unowned let apiManager: APIManager + private let EVENTS_KEY = "events" + private var _events:[String:Int] = [:] + private var events:[String:Int] { + get { + apiManager.propertiesSerialAccessQueue.sync { _events } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _events = newValue + } + } } - } - - private unowned let apiManager: APIManager - private let EVENTS_KEY = "events" - private var events:[String:Int] = [:] - private var timer:Timer? - - init(apiManager: APIManager) { - self.apiManager = apiManager - events = UserDefaults.standard.dictionary(forKey: EVENTS_KEY) as? [String:Int] ?? [:] - setupTimer() - } - - /// Counts the instances of a `Flag` being queried. - func trackEvent(flagName:String) { - let current = events[flagName] ?? 0 - events[flagName] = current + 1 - saveEvents() - } - - /// Invalidate and re-schedule timer for processing events - /// - /// On Apple (Darwin) platforms, this uses the Objective-C based - /// target/selector message sending API. - /// - /// Non-Darwin systems will use the corelibs Foundation block-based - /// api. Both platforms could use this approach, but the podspec - /// declares iOS 8.0 as a minimum target, and that api is only - /// available on 10+. (12.0 would be a good base in the future). - private func setupTimer() { - timer?.invalidate() - #if canImport(ObjectiveC) - timer = Timer.scheduledTimer( - timeInterval: TimeInterval(flushPeriod), - target: self, - selector: #selector(postAnalyticsWhenEnabled(_:)), - userInfo: nil, - repeats: true - ) - #else - timer = Timer.scheduledTimer( - withTimeInterval: TimeInterval(flushPeriod), - repeats: true, - block: { [weak self] _ in - self?.postAnalytics() - } - ) - #endif - } - - /// Reset events after successful processing. - private func reset() { - events = [:] - saveEvents() - } - - /// Persist the events to storage. - private func saveEvents() { - UserDefaults.standard.set(events, forKey: EVENTS_KEY) - } - - /// Send analytics to the api when enabled. - private func postAnalytics() { - guard enableAnalytics else { - return + private var _timer:Timer? + private var timer:Timer? { + get { + apiManager.propertiesSerialAccessQueue.sync { _timer } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _timer = newValue + } + } } - - guard !events.isEmpty else { - return + + init(apiManager: APIManager) { + self.apiManager = apiManager + events = UserDefaults.standard.dictionary(forKey: EVENTS_KEY) as? [String:Int] ?? [:] + setupTimer() + } + + /// Counts the instances of a `Flag` being queried. + func trackEvent(flagName:String) { + let current = events[flagName] ?? 0 + events[flagName] = current + 1 + saveEvents() + } + + /// Invalidate and re-schedule timer for processing events + /// + /// On Apple (Darwin) platforms, this uses the Objective-C based + /// target/selector message sending API. + /// + /// Non-Darwin systems will use the corelibs Foundation block-based + /// api. Both platforms could use this approach, but the podspec + /// declares iOS 8.0 as a minimum target, and that api is only + /// available on 10+. (12.0 would be a good base in the future). + private func setupTimer() { + timer?.invalidate() +#if canImport(ObjectiveC) + timer = Timer.scheduledTimer( + timeInterval: TimeInterval(flushPeriod), + target: self, + selector: #selector(postAnalyticsWhenEnabled(_:)), + userInfo: nil, + repeats: true + ) +#else + timer = Timer.scheduledTimer( + withTimeInterval: TimeInterval(flushPeriod), + repeats: true, + block: { [weak self] _ in + self?.postAnalytics() + } + ) +#endif } - - apiManager.request(.postAnalytics(events: events)) { [weak self] (result: Result) in - switch result { - case .failure: - print("Upload analytics failed") - case .success: - self?.reset() - } + + /// Reset events after successful processing. + private func reset() { + events = [:] + saveEvents() + } + + /// Persist the events to storage. + private func saveEvents() { + UserDefaults.standard.set(events, forKey: EVENTS_KEY) + } + + /// Send analytics to the api when enabled. + private func postAnalytics() { + guard enableAnalytics else { + return + } + + guard !events.isEmpty else { + return + } + + apiManager.request(.postAnalytics(events: events)) { @Sendable [weak self] (result: Result) in + switch result { + case .failure: + print("Upload analytics failed") + case .success: + self?.reset() + } + } + } + +#if canImport(ObjectiveC) + /// Event triggered when timer fired. + /// + /// Exposed on Apple platforms to relay selector-based events + @objc private func postAnalyticsWhenEnabled(_ timer: Timer) { + postAnalytics() } - } - - #if canImport(ObjectiveC) - /// Event triggered when timer fired. - /// - /// Exposed on Apple platforms to relay selector-based events - @objc private func postAnalyticsWhenEnabled(_ timer: Timer) { - postAnalytics() - } - #endif +#endif } diff --git a/FlagsmithClient/Classes/Trait.swift b/FlagsmithClient/Classes/Trait.swift index 85e9ec6..eda908e 100644 --- a/FlagsmithClient/Classes/Trait.swift +++ b/FlagsmithClient/Classes/Trait.swift @@ -43,14 +43,14 @@ public struct Trait: Codable, Sendable { self.identifier = identifier } - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: .key) typedValue = try container.decode(TypedValue.self, forKey: .value) identifier = nil } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(key, forKey: .key) try container.encode(typedValue, forKey: .value) diff --git a/FlagsmithClient/Classes/TypedValue.swift b/FlagsmithClient/Classes/TypedValue.swift index 5bd5794..b97ef4f 100644 --- a/FlagsmithClient/Classes/TypedValue.swift +++ b/FlagsmithClient/Classes/TypedValue.swift @@ -17,7 +17,7 @@ public enum TypedValue: Equatable, Sendable { } extension TypedValue: Codable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() if let value = try? container.decode(Bool.self) { @@ -49,10 +49,10 @@ extension TypedValue: Codable { codingPath: [], debugDescription: "No decodable `TypedValue` value found." ) - throw DecodingError.valueNotFound(Decodable.self, context) + throw DecodingError.valueNotFound((any Decodable).self, context) } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() switch self { case .bool(let value): diff --git a/FlagsmithClient/Classes/UnknownTypeValue.swift b/FlagsmithClient/Classes/UnknownTypeValue.swift index 7ff2ed0..b8f01b6 100644 --- a/FlagsmithClient/Classes/UnknownTypeValue.swift +++ b/FlagsmithClient/Classes/UnknownTypeValue.swift @@ -15,7 +15,7 @@ public enum UnknownTypeValue: Decodable, Sendable { case int(Int), string(String), float(Float), null - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { if let int = try? decoder.singleValueContainer().decode(Int.self) { self = .int(int) return diff --git a/FlagsmithClient/Tests/APIManagerTests.swift b/FlagsmithClient/Tests/APIManagerTests.swift index 435e74f..c001b5a 100644 --- a/FlagsmithClient/Tests/APIManagerTests.swift +++ b/FlagsmithClient/Tests/APIManagerTests.swift @@ -17,23 +17,22 @@ final class APIManagerTests: FlagsmithClientTestCase { apiManager.apiKey = nil let requestFinished = expectation(description: "Request Finished") - var error: FlagsmithError? - apiManager.request(.getFlags) { (result: Result) in + apiManager.request(.getFlags) { (result: Result) in if case let .failure(e) = result { - error = e as? FlagsmithError + let error = e as? FlagsmithError + let flagsmithError = try! XCTUnwrap(error) + guard case .apiKey = flagsmithError else { + XCTFail("Wrong Error") + requestFinished.fulfill() + return + } } requestFinished.fulfill() } wait(for: [requestFinished], timeout: 1.0) - - let flagsmithError = try XCTUnwrap(error) - guard case .apiKey = flagsmithError else { - XCTFail("Wrong Error") - return - } } /// Verify that an invalid API url produces the expected error. @@ -42,40 +41,42 @@ final class APIManagerTests: FlagsmithClientTestCase { apiManager.baseURL = URL(fileURLWithPath: "/dev/null") let requestFinished = expectation(description: "Request Finished") - var error: FlagsmithError? - - apiManager.request(.getFlags) { (result: Result) in + + apiManager.request(.getFlags) { (result: Result) in if case let .failure(e) = result { - error = e as? FlagsmithError + let error = e as? FlagsmithError + let flagsmithError: FlagsmithError? = try! XCTUnwrap(error) + guard case .apiURL = flagsmithError else { + XCTFail("Wrong Error") + requestFinished.fulfill() + return + } } requestFinished.fulfill() } wait(for: [requestFinished], timeout: 1.0) - - let flagsmithError = try XCTUnwrap(error) - guard case .apiURL = flagsmithError else { - XCTFail("Wrong Error") - return - } + } - + + @MainActor func testConcurrentRequests() throws { apiManager.apiKey = "8D5ABC87-6BBF-4AE7-BC05-4DC1AFE770DF" let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent) var expectations:[XCTestExpectation] = []; let iterations = 500 - var error: FlagsmithError? for concurrentIteration in 1...iterations { let expectation = XCTestExpectation(description: "Multiple threads can access the APIManager \(concurrentIteration)") expectations.append(expectation) concurrentQueue.async { - self.apiManager.request(.getFlags) { (result: Result) in + self.apiManager.request(.getFlags) { (result: Result) in if case let .failure(e) = result { - error = e as? FlagsmithError + let error = e as? FlagsmithError + // Ensure that we didn't have any errors during the process + XCTAssertTrue(error == nil) } expectation.fulfill() } @@ -83,8 +84,6 @@ final class APIManagerTests: FlagsmithClientTestCase { } wait(for: expectations, timeout: 10) - // Ensure that we didn't have any errors during the process - XCTAssertTrue(error == nil) print("Finished!") } diff --git a/Package.swift b/Package.swift index 84a8f31..fc3cb06 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.9 import PackageDescription @@ -11,10 +11,14 @@ let package = Package( .target( name: "FlagsmithClient", dependencies: [], - path: "FlagsmithClient/Classes"), + path: "FlagsmithClient/Classes", + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + .enableUpcomingFeature("ExistentialAny"), // https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md + ]), .testTarget( name: "FlagsmitClientTests", dependencies: ["FlagsmithClient"], - path: "FlagsmithClient/Tests"), + path: "FlagsmithClient/Tests") ] ) From f1043617d4a36613c0a5a9b5d69646f82a0711c6 Mon Sep 17 00:00:00 2001 From: 3a4oT Date: Tue, 12 Mar 2024 20:09:46 +0200 Subject: [PATCH 02/10] adjusted macOS runner --- .github/workflows/pull-request.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index bd5a175..c964181 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -8,10 +8,10 @@ on: jobs: macos-build: - runs-on: macos-latest + runs-on: macOS-13 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Build (macOS) run: swift build -v - name: Run tests @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Build (Ubuntu) run: swift build -v - name: Run tests From bad20786f4a56ca6dcb0d78351a3b4d5e6dff74e Mon Sep 17 00:00:00 2001 From: 3a4oT Date: Tue, 12 Mar 2024 20:14:43 +0200 Subject: [PATCH 03/10] cleanup --- FlagsmithClient/Tests/APIManagerTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/FlagsmithClient/Tests/APIManagerTests.swift b/FlagsmithClient/Tests/APIManagerTests.swift index c001b5a..083e2f8 100644 --- a/FlagsmithClient/Tests/APIManagerTests.swift +++ b/FlagsmithClient/Tests/APIManagerTests.swift @@ -60,7 +60,6 @@ final class APIManagerTests: FlagsmithClientTestCase { } - @MainActor func testConcurrentRequests() throws { apiManager.apiKey = "8D5ABC87-6BBF-4AE7-BC05-4DC1AFE770DF" let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent) From 6b5dc80a159e04252c1265f1f7f1696f9ca94e1f Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Tue, 2 Apr 2024 09:47:10 +0100 Subject: [PATCH 04/10] Update podspec to match Package.swift, update to release 3.6.0 --- .../xcshareddata/WorkspaceSettings.xcsettings | 5 + Example/Podfile.lock | 6 +- .../FlagsmithClient.podspec.json | 8 +- Example/Pods/Manifest.lock | 6 +- Example/Pods/Pods.xcodeproj/project.pbxproj | 134 +++++++++--------- .../FlagsmithClient-Info.plist | 2 +- FlagsmithClient.podspec | 4 +- 7 files changed, 85 insertions(+), 80 deletions(-) create mode 100644 Example/FlagsmithClient.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/Example/FlagsmithClient.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Example/FlagsmithClient.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/Example/FlagsmithClient.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 735d4f9..d51e33c 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - FlagsmithClient (3.4.0) + - FlagsmithClient (3.6.0) DEPENDENCIES: - FlagsmithClient (from `../`) @@ -9,8 +9,8 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - FlagsmithClient: 0f8ed4a38dec385d73cc21a64b791b39bcc8c32b + FlagsmithClient: 3a96576f5a251c807e6aa0a3b0db55b3e1dfd0a3 PODFILE CHECKSUM: 9fc876dee0cf031cae843156b0740a94b4994d8c -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/Example/Pods/Local Podspecs/FlagsmithClient.podspec.json b/Example/Pods/Local Podspecs/FlagsmithClient.podspec.json index 3081ee8..02921d4 100644 --- a/Example/Pods/Local Podspecs/FlagsmithClient.podspec.json +++ b/Example/Pods/Local Podspecs/FlagsmithClient.podspec.json @@ -1,6 +1,6 @@ { "name": "FlagsmithClient", - "version": "3.4.0", + "version": "3.6.0", "summary": "iOS Client written in Swift for Flagsmith. Ship features with confidence using feature flags and remote config.", "homepage": "https://github.com/Flagsmith/flagsmith-ios-client", "license": { @@ -12,13 +12,13 @@ }, "source": { "git": "https://github.com/Flagsmith/flagsmith-ios-client.git", - "tag": "3.4.0" + "tag": "3.6.0" }, "social_media_url": "https://twitter.com/getflagsmith", "platforms": { "ios": "12.0" }, "source_files": "FlagsmithClient/Classes/**/*", - "swift_versions": "4.0", - "swift_version": "4.0" + "swift_versions": "5.6", + "swift_version": "5.6" } diff --git a/Example/Pods/Manifest.lock b/Example/Pods/Manifest.lock index 735d4f9..d51e33c 100644 --- a/Example/Pods/Manifest.lock +++ b/Example/Pods/Manifest.lock @@ -1,5 +1,5 @@ PODS: - - FlagsmithClient (3.4.0) + - FlagsmithClient (3.6.0) DEPENDENCIES: - FlagsmithClient (from `../`) @@ -9,8 +9,8 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - FlagsmithClient: 0f8ed4a38dec385d73cc21a64b791b39bcc8c32b + FlagsmithClient: 3a96576f5a251c807e6aa0a3b0db55b3e1dfd0a3 PODFILE CHECKSUM: 9fc876dee0cf031cae843156b0740a94b4994d8c -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index 6909f18..fd4ae47 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -373,71 +373,6 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ - 0B1CDA5D382F4F3C96E9AE205B213EE1 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 8236C25B17E89C77288367CCF5B829A9 /* FlagsmithClient.debug.xcconfig */; - buildSettings = { - ARCHS = "$(ARCHS_STANDARD_64_BIT)"; - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/FlagsmithClient/FlagsmithClient-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MODULEMAP_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient.modulemap"; - PRODUCT_MODULE_NAME = FlagsmithClient; - PRODUCT_NAME = FlagsmithClient; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 12BB8FAF3E0B6864216E35B141237E9A /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = B27C6884FC1F031C5CCE03E04DA59B40 /* FlagsmithClient.release.xcconfig */; - buildSettings = { - ARCHS = "$(ARCHS_STANDARD_64_BIT)"; - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/FlagsmithClient/FlagsmithClient-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MODULEMAP_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient.modulemap"; - PRODUCT_MODULE_NAME = FlagsmithClient; - PRODUCT_NAME = FlagsmithClient; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 4.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; 2B9E26EAE2CD392AD762421F663075A1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -504,6 +439,39 @@ }; name = Debug; }; + 2FEE03729C8CD1FA45680EB6B9941372 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B27C6884FC1F031C5CCE03E04DA59B40 /* FlagsmithClient.release.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/FlagsmithClient/FlagsmithClient-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient.modulemap"; + PRODUCT_MODULE_NAME = FlagsmithClient; + PRODUCT_NAME = FlagsmithClient; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.6; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; 63FAF33E1C55B71A5F5A8B3CC8749F99 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -600,6 +568,38 @@ }; name = Debug; }; + BDFC8E9E764D7F6D61D71A36E95488F3 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8236C25B17E89C77288367CCF5B829A9 /* FlagsmithClient.debug.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/FlagsmithClient/FlagsmithClient-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient.modulemap"; + PRODUCT_MODULE_NAME = FlagsmithClient; + PRODUCT_NAME = FlagsmithClient; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.6; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; D22AD683643CD18DDDA1624DDA6590F4 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 250DE57229233B0BAD273A076F108A0E /* Pods-FlagsmithClient_Example.release.xcconfig */; @@ -659,8 +659,8 @@ CE8A22033473608C91867F497BC5A2CD /* Build configuration list for PBXNativeTarget "FlagsmithClient" */ = { isa = XCConfigurationList; buildConfigurations = ( - 0B1CDA5D382F4F3C96E9AE205B213EE1 /* Debug */, - 12BB8FAF3E0B6864216E35B141237E9A /* Release */, + BDFC8E9E764D7F6D61D71A36E95488F3 /* Debug */, + 2FEE03729C8CD1FA45680EB6B9941372 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Example/Pods/Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist b/Example/Pods/Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist index 4e1b060..05a2e18 100644 --- a/Example/Pods/Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist +++ b/Example/Pods/Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.4.0 + 3.6.0 CFBundleSignature ???? CFBundleVersion diff --git a/FlagsmithClient.podspec b/FlagsmithClient.podspec index 92bf9cb..a657ef4 100644 --- a/FlagsmithClient.podspec +++ b/FlagsmithClient.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'FlagsmithClient' - s.version = '3.5.0' + s.version = '3.6.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' } @@ -19,5 +19,5 @@ Pod::Spec.new do |s| s.ios.deployment_target = '12.0' s.source_files = 'FlagsmithClient/Classes/**/*' - s.swift_versions = '4.0' + s.swift_versions = '5.6' end From b34ec0dc74a138e4cff95f6503863745ccfc82fa Mon Sep 17 00:00:00 2001 From: Petro Rovenskyy Date: Mon, 22 Apr 2024 15:56:24 +0300 Subject: [PATCH 05/10] "StrictConcurrency=complete" and preparation for upcoming Swift 6 (#48) * - Bumped `swift-tools` to swift 5.9. - Enabled `StrictConcurrency=complete` mode in order to be prepared for upcoming Swift 6. Implemented `Sendable` support for Flagsmith client and friends. - Enabled `ExistentialAny` feature in order to be prepared for upcoming Swift 6. Code adjustments - adjusted tests for `StrictConcurrency=complete` mode . * adjusted macOS runner * cleanup --- .github/workflows/pull-request.yml | 6 +- FlagsmithClient/Classes/Feature.swift | 2 +- FlagsmithClient/Classes/Flag.swift | 2 +- FlagsmithClient/Classes/Flagsmith.swift | 496 +++++++++--------- FlagsmithClient/Classes/FlagsmithError.swift | 6 +- .../Classes/Internal/APIManager.swift | 281 +++++----- .../Classes/Internal/FlagsmithAnalytics.swift | 226 ++++---- FlagsmithClient/Classes/Trait.swift | 4 +- FlagsmithClient/Classes/TypedValue.swift | 6 +- .../Classes/UnknownTypeValue.swift | 2 +- FlagsmithClient/Tests/APIManagerTests.swift | 48 +- Package.swift | 10 +- 12 files changed, 591 insertions(+), 498 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index bd5a175..c964181 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -8,10 +8,10 @@ on: jobs: macos-build: - runs-on: macos-latest + runs-on: macOS-13 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Build (macOS) run: swift build -v - name: Run tests @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Build (Ubuntu) run: swift build -v - name: Run tests diff --git a/FlagsmithClient/Classes/Feature.swift b/FlagsmithClient/Classes/Feature.swift index 446bb4f..adf3473 100644 --- a/FlagsmithClient/Classes/Feature.swift +++ b/FlagsmithClient/Classes/Feature.swift @@ -28,7 +28,7 @@ public struct Feature: Codable, Sendable { self.description = description } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.name, forKey: .name) try container.encodeIfPresent(self.type, forKey: .type) diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index b9a9ebc..1bd63f5 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -47,7 +47,7 @@ public struct Flag: Codable, Sendable { self.enabled = enabled } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.feature, forKey: .feature) try container.encode(self.value, forKey: .value) diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index 495fff6..777d489 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -12,269 +12,291 @@ import FoundationNetworking /// Manage feature flags and remote config across multiple projects, /// environments and organisations. -public class Flagsmith { - /// Shared singleton client object - public static let shared = Flagsmith() - private let apiManager = APIManager() - private lazy var analytics = FlagsmithAnalytics(apiManager: apiManager) - - /// Base URL - /// - /// The default implementation uses: `https://edge.api.flagsmith.com/api/v1`. - public var baseURL: URL { - set { apiManager.baseURL = newValue } - get { apiManager.baseURL } - } - - /// API Key unique to your organization. - /// - /// This value must be provided before any request can succeed. - public var apiKey: String? { - set { apiManager.apiKey = newValue } - get { apiManager.apiKey } - } +public final class Flagsmith: @unchecked Sendable { + /// Shared singleton client object + public static let shared: Flagsmith = Flagsmith() + private let apiManager: APIManager + private let analytics: FlagsmithAnalytics - /// Is flag analytics enabled? - public var enableAnalytics: Bool { - set { analytics.enableAnalytics = newValue } - get { analytics.enableAnalytics } - } + /// Base URL + /// + /// The default implementation uses: `https://edge.api.flagsmith.com/api/v1`. + public var baseURL: URL { + set { apiManager.baseURL = newValue } + get { apiManager.baseURL } + } + + /// API Key unique to your organization. + /// + /// This value must be provided before any request can succeed. + public var apiKey: String? { + set { apiManager.apiKey = newValue } + get { apiManager.apiKey } + } + + /// Is flag analytics enabled? + public var enableAnalytics: Bool { + set { analytics.enableAnalytics = newValue } + get { analytics.enableAnalytics } + } - /// How often to send the flag analytics, in seconds - public var analyticsFlushPeriod: Int { - 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() + /// How often to send the flag analytics, in seconds + public var analyticsFlushPeriod: Int { + set { analytics.flushPeriod = newValue } + get { analytics.flushPeriod } + } - private init() { - } - - /// Get all feature flags (flags and remote config) optionally for a specific identity - /// - /// - Parameters: - /// - identity: ID of the user (optional) - /// - 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 thisIdentity): - completion(.success(thisIdentity.flags)) - case .failure(let error): - if self.defaultFlags.isEmpty { - completion(.failure(error)) - } - else { - completion(.success(self.defaultFlags)) - } + /// Default flags to fall back on if an API call fails + private var _defaultFlags: [Flag] = [] + public var defaultFlags: [Flag] { + get { + apiManager.propertiesSerialAccessQueue.sync { _defaultFlags } } - } - } else { - apiManager.request(.getFlags) { (result: Result<[Flag], Error>) in - 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)) - } + set { + apiManager.propertiesSerialAccessQueue.sync { + _defaultFlags = newValue + } } - } } - } - - /// Check feature exists and is enabled optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - completion: Closure with Result which contains Bool in case of success or Error in case of failure - public func hasFeatureFlag(withID id: String, - forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) { - analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in - switch result { - case .success(let flags): - let hasFlag = flags.contains(where: {$0.feature.name == id && $0.enabled}) - completion(.success(hasFlag)) - case .failure(let error): - if self.defaultFlags.contains(where: {$0.feature.name == id && $0.enabled}) { - completion(.success(true)) + + /// Configuration class for the cache settings + private var _cacheConfig: CacheConfig = CacheConfig() + public var cacheConfig: CacheConfig { + get { + apiManager.propertiesSerialAccessQueue.sync { _cacheConfig } } - else { - completion(.failure(error)) + set { + apiManager.propertiesSerialAccessQueue.sync { + _cacheConfig = newValue + } } - } } - } - - /// Get remote config value optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - completion: Closure with Result which String in case of success or Error in case of failure - @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") - public func getFeatureValue(withID id: String, - forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) { - analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in - switch result { - case .success(let flags): - let flag = flags.first(where: {$0.feature.name == id}) - completion(.success(flag?.value.stringValue)) - case .failure(let error): - if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { - completion(.success(flag.value.stringValue)) + + private init() { + apiManager = APIManager() + analytics = FlagsmithAnalytics(apiManager: apiManager) + } + + /// Get all feature flags (flags and remote config) optionally for a specific identity + /// + /// - Parameters: + /// - identity: ID of the user (optional) + /// - 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: @Sendable @escaping (Result<[Flag], any Error>) -> Void) { + + if let identity = identity { + getIdentity(identity) { (result) in + switch result { + case .success(let thisIdentity): + completion(.success(thisIdentity.flags)) + case .failure(let error): + if self.defaultFlags.isEmpty { + completion(.failure(error)) + } + else { + completion(.success(self.defaultFlags)) + } + } + } + } else { + apiManager.request(.getFlags) { (result: Result<[Flag], Error>) in + 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)) + } + } + } } - else { - completion(.failure(error)) + } + + /// Check feature exists and is enabled optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - completion: Closure with Result which contains Bool in case of success or Error in case of failure + public func hasFeatureFlag(withID id: String, + forIdentity identity: String? = nil, + completion: @Sendable @escaping (Result) -> Void) { + analytics.trackEvent(flagName: id) + getFeatureFlags(forIdentity: identity) { (result) in + switch result { + case .success(let flags): + let hasFlag = flags.contains(where: {$0.feature.name == id && $0.enabled}) + completion(.success(hasFlag)) + case .failure(let error): + if self.defaultFlags.contains(where: {$0.feature.name == id && $0.enabled}) { + completion(.success(true)) + } + else { + completion(.failure(error)) + } + } } - } } - } - - /// Get remote config value optionally for a specific identity - /// - /// - Parameters: - /// - id: ID of the feature - /// - identity: ID of the user (optional) - /// - completion: Closure with Result of `TypedValue` in case of success or `Error` in case of failure - public func getValueForFeature(withID id: String, - forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) { - analytics.trackEvent(flagName: id) - getFeatureFlags(forIdentity: identity) { (result) in - switch result { - case .success(let flags): - let flag = flags.first(where: {$0.feature.name == id}) - completion(.success(flag?.value)) - case .failure(let error): - if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { - completion(.success(flag.value)) + + /// Get remote config value optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - completion: Closure with Result which String in case of success or Error in case of failure + @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") + public func getFeatureValue(withID id: String, + forIdentity identity: String? = nil, + completion: @Sendable @escaping (Result) -> Void) { + analytics.trackEvent(flagName: id) + getFeatureFlags(forIdentity: identity) { (result) in + switch result { + case .success(let flags): + let flag = flags.first(where: {$0.feature.name == id}) + completion(.success(flag?.value.stringValue)) + case .failure(let error): + if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { + completion(.success(flag.value.stringValue)) + } + else { + completion(.failure(error)) + } + } } - else { - completion(.failure(error)) + } + + /// Get remote config value optionally for a specific identity + /// + /// - Parameters: + /// - id: ID of the feature + /// - identity: ID of the user (optional) + /// - completion: Closure with Result of `TypedValue` in case of success or `Error` in case of failure + public func getValueForFeature(withID id: String, + forIdentity identity: String? = nil, + completion: @Sendable @escaping (Result) -> Void) { + analytics.trackEvent(flagName: id) + getFeatureFlags(forIdentity: identity) { (result) in + switch result { + case .success(let flags): + let flag = flags.first(where: {$0.feature.name == id}) + completion(.success(flag?.value)) + case .failure(let error): + if let flag = self.getFlagUsingDefaults(withID: id, forIdentity: identity) { + completion(.success(flag.value)) + } + else { + completion(.failure(error)) + } + } } - } } - } - - /// Get all user traits for provided identity. Optionally filter results with a list of keys - /// - /// - Parameters: - /// - ids: IDs of the trait (optional) - /// - identity: ID of the user - /// - completion: Closure with Result which contains array of Trait objects in case of success or Error in case of failure - public func getTraits(withIDS ids: [String]? = nil, - forIdentity identity: String, - completion: @escaping (Result<[Trait], Error>) -> Void) { - getIdentity(identity) { (result) in - switch result { - case .success(let identity): - if let ids = ids { - let traits = identity.traits.filter({ids.contains($0.key)}) - completion(.success(traits)) - } else { - completion(.success(identity.traits)) + + /// Get all user traits for provided identity. Optionally filter results with a list of keys + /// + /// - Parameters: + /// - ids: IDs of the trait (optional) + /// - identity: ID of the user + /// - completion: Closure with Result which contains array of Trait objects in case of success or Error in case of failure + public func getTraits(withIDS ids: [String]? = nil, + forIdentity identity: String, + completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) { + getIdentity(identity) { (result) in + switch result { + case .success(let identity): + if let ids = ids { + let traits = identity.traits.filter({ids.contains($0.key)}) + completion(.success(traits)) + } else { + completion(.success(identity.traits)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + /// Get user trait for provided identity and trait key + /// + /// - Parameters: + /// - id: ID of the trait + /// - identity: ID of the user + /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure + public func getTrait(withID id: String, + forIdentity identity: String, + completion: @Sendable @escaping (Result) -> Void) { + getIdentity(identity) { (result) in + switch result { + case .success(let identity): + let trait = identity.traits.first(where: {$0.key == id}) + completion(.success(trait)) + case .failure(let error): + completion(.failure(error)) + } } - case .failure(let error): - completion(.failure(error)) - } } - } - - /// Get user trait for provided identity and trait key - /// - /// - Parameters: - /// - id: ID of the trait - /// - identity: ID of the user - /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure - public func getTrait(withID id: String, - forIdentity identity: String, - completion: @escaping (Result) -> Void) { - getIdentity(identity) { (result) in - switch result { - case .success(let identity): - let trait = identity.traits.first(where: {$0.key == id}) - completion(.success(trait)) - case .failure(let error): - completion(.failure(error)) - } + + /// Set user trait for provided identity + /// + /// - Parameters: + /// - trait: Trait to be created or updated + /// - identity: ID of the user + /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure + public func setTrait(_ trait: Trait, + forIdentity identity: String, + completion: @Sendable @escaping (Result) -> Void) { + apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in + completion(result) + } } - } - - /// Set user trait for provided identity - /// - /// - Parameters: - /// - trait: Trait to be created or updated - /// - identity: ID of the user - /// - completion: Closure with Result which contains Trait in case of success or Error in case of failure - public func setTrait(_ trait: Trait, - forIdentity identity: String, - completion: @escaping (Result) -> Void) { - apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in - completion(result) + + /// Set user traits in bulk for provided identity + /// + /// - Parameters: + /// - traits: Traits to be created or updated + /// - identity: ID of the user + /// - completion: Closure with Result which contains Traits in case of success or Error in case of failure + public func setTraits(_ traits: [Trait], + forIdentity identity: String, + completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) { + apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in + completion(result.map(\.traits)) + } } - } - /// Set user traits in bulk for provided identity - /// - /// - Parameters: - /// - traits: Traits to be created or updated - /// - identity: ID of the user - /// - completion: Closure with Result which contains Traits in case of success or Error in case of failure - public func setTraits(_ traits: [Trait], - forIdentity identity: String, - completion: @escaping (Result<[Trait], Error>) -> Void) { - apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in - completion(result.map(\.traits)) + /// Get both feature flags and user traits for the provided identity + /// + /// - Parameters: + /// - identity: ID of the user + /// - completion: Closure with Result which contains Identity in case of success or Error in case of failure + public func getIdentity(_ identity: String, + completion: @Sendable @escaping (Result) -> Void) { + apiManager.request(.getIdentity(identity: identity)) { (result: Result) in + completion(result) + } } - } - - /// Get both feature flags and user traits for the provided identity - /// - /// - Parameters: - /// - identity: ID of the user - /// - completion: Closure with Result which contains Identity in case of success or Error in case of failure - public func getIdentity(_ identity: String, - completion: @escaping (Result) -> Void) { - apiManager.request(.getIdentity(identity: identity)) { (result: Result) in - 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}) } - } - - /// 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 { +public final class CacheConfig { + + /// Cache to use when enabled, defaults to the shared app cache + public var cache: URLCache = URLCache.shared - /// 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 - /// 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 - /// 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 - /// Skip API if there is a cache available - public var skipAPI: Bool = false - } diff --git a/FlagsmithClient/Classes/FlagsmithError.swift b/FlagsmithClient/Classes/FlagsmithError.swift index fda9675..672d4b2 100644 --- a/FlagsmithClient/Classes/FlagsmithError.swift +++ b/FlagsmithClient/Classes/FlagsmithError.swift @@ -20,8 +20,8 @@ public enum FlagsmithError: LocalizedError, Sendable { /// API Response could not be decoded. case decoding(DecodingError) /// Unknown or unhandled error was encountered. - case unhandled(Error) - + case unhandled(any Error) + public var errorDescription: String? { switch self { case .apiKey: @@ -46,7 +46,7 @@ public enum FlagsmithError: LocalizedError, Sendable { /// * as `EncodingError`: `.encoding()` error will be created. /// * as `DecodingError`: `.decoding()` error will be created. /// * default: `.unhandled()` error will be created. - internal init(_ error: Error) { + internal init(_ error: any Error) { switch error { case let flagsmithError as FlagsmithError: self = flagsmithError diff --git a/FlagsmithClient/Classes/Internal/APIManager.swift b/FlagsmithClient/Classes/Internal/APIManager.swift index b13b898..d5bb506 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -11,142 +11,173 @@ import FoundationNetworking #endif /// Handles interaction with a **Flagsmith** api. -class APIManager : NSObject, URLSessionDataDelegate { - - 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:[Int:(Result) -> Void] = [:] - private var tasksToData:[Int:Data] = [:] - private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue") - - override init() { - super.init() - let configuration = URLSessionConfiguration.default - self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) - } - - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - serialAccessQueue.sync { - if let dataTask = task as? URLSessionDataTask { - if let completion = tasksToCompletionHandlers[dataTask.taskIdentifier] { - if let error = error { - DispatchQueue.main.async { completion(.failure(FlagsmithError.unhandled(error))) } - } - else { - let data = tasksToData[dataTask.taskIdentifier] ?? Data() - DispatchQueue.main.async { completion(.success(data)) } - } +final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { + + private var _session: URLSession! + private var session: URLSession { + get { + propertiesSerialAccessQueue.sync { _session } + } + set { + propertiesSerialAccessQueue.sync(flags: .barrier) { + _session = newValue + } } - tasksToCompletionHandlers[dataTask.taskIdentifier] = nil - tasksToData[dataTask.taskIdentifier] = nil - } - } - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, - completionHandler: @escaping (CachedURLResponse?) -> Void) { - serialAccessQueue.sync { - // intercept and modify the cache settings for the response - if Flagsmith.shared.cacheConfig.useCache { - let newResponse = proposedResponse.response(withExpirationDuration: Int(Flagsmith.shared.cacheConfig.cacheTTL)) - DispatchQueue.main.async { completionHandler(newResponse) } - } else { - DispatchQueue.main.async { completionHandler(proposedResponse) } - } } - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - serialAccessQueue.sync { - var existingData = tasksToData[dataTask.taskIdentifier] ?? Data() - existingData.append(data) - tasksToData[dataTask.taskIdentifier] = existingData + + /// Base `URL` used for requests. + private var _baseURL = URL(string: "https://edge.api.flagsmith.com/api/v1/")! + var baseURL: URL { + get { + propertiesSerialAccessQueue.sync { _baseURL } + } + set { + propertiesSerialAccessQueue.sync { + _baseURL = newValue + } + } } - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - completionHandler(.allow) + /// API Key unique to an organization. + private var _apiKey: String? + var apiKey: String? { + get { + propertiesSerialAccessQueue.sync { _apiKey } + } + set { + propertiesSerialAccessQueue.sync { + _apiKey = newValue + } + } } - - /// Base request method that handles creating a `URLRequest` and processing - /// the `URLSession` response. - /// - /// - parameters: - /// - router: The path and parameters that should be requested. - /// - completion: Function block executed with the result of the request. - private func request(_ router: Router, completion: @escaping (Result) -> Void) { - guard let apiKey = apiKey, !apiKey.isEmpty else { - completion(.failure(FlagsmithError.apiKey)) - return + + // store the completion handlers and accumulated data for each task + private var tasksToCompletionHandlers:[Int: @Sendable(Result) -> Void] = [:] + private var tasksToData:[Int:Data] = [:] + private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue") + let propertiesSerialAccessQueue = DispatchQueue(label: "propertiesSerialAccessQueue") + + override init() { + super.init() + let configuration = URLSessionConfiguration.default + self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) } - - var request: URLRequest - do { - request = try router.request(baseUrl: baseURL, apiKey: apiKey) - } catch { - completion(.failure(error)) - return + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { + serialAccessQueue.sync { + if let dataTask = task as? URLSessionDataTask { + if let completion = tasksToCompletionHandlers[dataTask.taskIdentifier] { + if let error = error { + DispatchQueue.main.async { completion(.failure(FlagsmithError.unhandled(error))) } + } + else { + let data = tasksToData[dataTask.taskIdentifier] ?? Data() + DispatchQueue.main.async { completion(.success(data)) } + } + } + tasksToCompletionHandlers[dataTask.taskIdentifier] = nil + tasksToData[dataTask.taskIdentifier] = nil + } + } } - - // 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 - } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, + completionHandler: @Sendable @escaping (CachedURLResponse?) -> Void) { + serialAccessQueue.sync { + // intercept and modify the cache settings for the response + if Flagsmith.shared.cacheConfig.useCache { + let newResponse = proposedResponse.response(withExpirationDuration: Int(Flagsmith.shared.cacheConfig.cacheTTL)) + DispatchQueue.main.async { completionHandler(newResponse) } + } else { + DispatchQueue.main.async { completionHandler(proposedResponse) } + } + } } - - // we must use the delegate form here, not the completion handler, to be able to modify the cache - serialAccessQueue.sync { - let task = session.dataTask(with: request) - tasksToCompletionHandlers[task.taskIdentifier] = completion - task.resume() + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + serialAccessQueue.sync { + var existingData = tasksToData[dataTask.taskIdentifier] ?? Data() + existingData.append(data) + tasksToData[dataTask.taskIdentifier] = existingData + } } - } - - /// Requests a api route and only relays success or failure of the action. - /// - /// - parameters: - /// - router: The path and parameters that should be requested. - /// - completion: Function block executed with the result of the request. - func request(_ router: Router, completion: @escaping (Result) -> Void) { - request(router) { (result: Result) in - switch result { - case .failure(let error): - completion(.failure(FlagsmithError(error))) - case .success: - completion(.success(())) - } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + completionHandler(.allow) } - } - - /// Requests a api route and attempts the decode the response. - /// - /// - parameters: - /// - router: The path and parameters that should be requested. - /// - decoder: `JSONDecoder` used to deserialize the response data. - /// - completion: Function block executed with the result of the request. - func request(_ router: Router, using decoder: JSONDecoder = JSONDecoder(), completion: @escaping (Result) -> Void) { - request(router) { (result: Result) in - switch result { - case .failure(let error): - completion(.failure(error)) - case .success(let data): + + /// Base request method that handles creating a `URLRequest` and processing + /// the `URLSession` response. + /// + /// - parameters: + /// - router: The path and parameters that should be requested. + /// - completion: Function block executed with the result of the request. + private func request(_ router: Router, completion: @Sendable @escaping (Result) -> Void) { + guard let apiKey = apiKey, !apiKey.isEmpty else { + completion(.failure(FlagsmithError.apiKey)) + return + } + + var request: URLRequest do { - let value = try decoder.decode(T.self, from: data) - completion(.success(value)) + request = try router.request(baseUrl: baseURL, apiKey: apiKey) } catch { - completion(.failure(FlagsmithError(error))) + completion(.failure(error)) + 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 + } + } + + // we must use the delegate form here, not the completion handler, to be able to modify the cache + serialAccessQueue.sync { + let task = session.dataTask(with: request) + tasksToCompletionHandlers[task.taskIdentifier] = completion + task.resume() + } + } + + /// Requests a api route and only relays success or failure of the action. + /// + /// - parameters: + /// - router: The path and parameters that should be requested. + /// - completion: Function block executed with the result of the request. + func request(_ router: Router, completion: @Sendable @escaping (Result) -> Void) { + request(router) { (result: Result) in + switch result { + case .failure(let error): + completion(.failure(FlagsmithError(error))) + case .success: + completion(.success(())) + } + } + } + + /// Requests a api route and attempts the decode the response. + /// + /// - parameters: + /// - router: The path and parameters that should be requested. + /// - decoder: `JSONDecoder` used to deserialize the response data. + /// - completion: Function block executed with the result of the request. + func request(_ router: Router, using decoder: JSONDecoder = JSONDecoder(), completion: @Sendable @escaping (Result) -> Void) { + request(router) { (result: Result) in + switch result { + case .failure(let error): + completion(.failure(error)) + case .success(let data): + do { + let value = try decoder.decode(T.self, from: data) + completion(.success(value)) + } catch { + completion(.failure(FlagsmithError(error))) + } + } } - } } - } } diff --git a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift index d172b3b..1d6c3f0 100644 --- a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift +++ b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift @@ -8,102 +8,140 @@ import Foundation /// Internal analytics for the **FlagsmithClient** -class FlagsmithAnalytics { - - /// Indicates if analytics are enabled. - var enableAnalytics: Bool = true - /// How often analytics events are processed (in seconds). - var flushPeriod: Int = 10 { - didSet { - setupTimer() +final class FlagsmithAnalytics: @unchecked Sendable { + + /// Indicates if analytics are enabled. + private var _enableAnalytics: Bool = true + var enableAnalytics:Bool { + get { + apiManager.propertiesSerialAccessQueue.sync { _enableAnalytics } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _enableAnalytics = newValue + } + } + } + + private var _flushPeriod: Int = 10 + /// How often analytics events are processed (in seconds). + var flushPeriod: Int { + get { + apiManager.propertiesSerialAccessQueue.sync { _flushPeriod } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _flushPeriod = newValue + } + setupTimer() + } + } + + private unowned let apiManager: APIManager + private let EVENTS_KEY = "events" + private var _events:[String:Int] = [:] + private var events:[String:Int] { + get { + apiManager.propertiesSerialAccessQueue.sync { _events } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _events = newValue + } + } } - } - - private unowned let apiManager: APIManager - private let EVENTS_KEY = "events" - private var events:[String:Int] = [:] - private var timer:Timer? - - init(apiManager: APIManager) { - self.apiManager = apiManager - events = UserDefaults.standard.dictionary(forKey: EVENTS_KEY) as? [String:Int] ?? [:] - setupTimer() - } - - /// Counts the instances of a `Flag` being queried. - func trackEvent(flagName:String) { - let current = events[flagName] ?? 0 - events[flagName] = current + 1 - saveEvents() - } - - /// Invalidate and re-schedule timer for processing events - /// - /// On Apple (Darwin) platforms, this uses the Objective-C based - /// target/selector message sending API. - /// - /// Non-Darwin systems will use the corelibs Foundation block-based - /// api. Both platforms could use this approach, but the podspec - /// declares iOS 8.0 as a minimum target, and that api is only - /// available on 10+. (12.0 would be a good base in the future). - private func setupTimer() { - timer?.invalidate() - #if canImport(ObjectiveC) - timer = Timer.scheduledTimer( - timeInterval: TimeInterval(flushPeriod), - target: self, - selector: #selector(postAnalyticsWhenEnabled(_:)), - userInfo: nil, - repeats: true - ) - #else - timer = Timer.scheduledTimer( - withTimeInterval: TimeInterval(flushPeriod), - repeats: true, - block: { [weak self] _ in - self?.postAnalytics() - } - ) - #endif - } - - /// Reset events after successful processing. - private func reset() { - events = [:] - saveEvents() - } - - /// Persist the events to storage. - private func saveEvents() { - UserDefaults.standard.set(events, forKey: EVENTS_KEY) - } - - /// Send analytics to the api when enabled. - private func postAnalytics() { - guard enableAnalytics else { - return + private var _timer:Timer? + private var timer:Timer? { + get { + apiManager.propertiesSerialAccessQueue.sync { _timer } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _timer = newValue + } + } } - - guard !events.isEmpty else { - return + + init(apiManager: APIManager) { + self.apiManager = apiManager + events = UserDefaults.standard.dictionary(forKey: EVENTS_KEY) as? [String:Int] ?? [:] + setupTimer() + } + + /// Counts the instances of a `Flag` being queried. + func trackEvent(flagName:String) { + let current = events[flagName] ?? 0 + events[flagName] = current + 1 + saveEvents() + } + + /// Invalidate and re-schedule timer for processing events + /// + /// On Apple (Darwin) platforms, this uses the Objective-C based + /// target/selector message sending API. + /// + /// Non-Darwin systems will use the corelibs Foundation block-based + /// api. Both platforms could use this approach, but the podspec + /// declares iOS 8.0 as a minimum target, and that api is only + /// available on 10+. (12.0 would be a good base in the future). + private func setupTimer() { + timer?.invalidate() +#if canImport(ObjectiveC) + timer = Timer.scheduledTimer( + timeInterval: TimeInterval(flushPeriod), + target: self, + selector: #selector(postAnalyticsWhenEnabled(_:)), + userInfo: nil, + repeats: true + ) +#else + timer = Timer.scheduledTimer( + withTimeInterval: TimeInterval(flushPeriod), + repeats: true, + block: { [weak self] _ in + self?.postAnalytics() + } + ) +#endif } - - apiManager.request(.postAnalytics(events: events)) { [weak self] (result: Result) in - switch result { - case .failure: - print("Upload analytics failed") - case .success: - self?.reset() - } + + /// Reset events after successful processing. + private func reset() { + events = [:] + saveEvents() + } + + /// Persist the events to storage. + private func saveEvents() { + UserDefaults.standard.set(events, forKey: EVENTS_KEY) + } + + /// Send analytics to the api when enabled. + private func postAnalytics() { + guard enableAnalytics else { + return + } + + guard !events.isEmpty else { + return + } + + apiManager.request(.postAnalytics(events: events)) { @Sendable [weak self] (result: Result) in + switch result { + case .failure: + print("Upload analytics failed") + case .success: + self?.reset() + } + } + } + +#if canImport(ObjectiveC) + /// Event triggered when timer fired. + /// + /// Exposed on Apple platforms to relay selector-based events + @objc private func postAnalyticsWhenEnabled(_ timer: Timer) { + postAnalytics() } - } - - #if canImport(ObjectiveC) - /// Event triggered when timer fired. - /// - /// Exposed on Apple platforms to relay selector-based events - @objc private func postAnalyticsWhenEnabled(_ timer: Timer) { - postAnalytics() - } - #endif +#endif } diff --git a/FlagsmithClient/Classes/Trait.swift b/FlagsmithClient/Classes/Trait.swift index 85e9ec6..eda908e 100644 --- a/FlagsmithClient/Classes/Trait.swift +++ b/FlagsmithClient/Classes/Trait.swift @@ -43,14 +43,14 @@ public struct Trait: Codable, Sendable { self.identifier = identifier } - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: .key) typedValue = try container.decode(TypedValue.self, forKey: .value) identifier = nil } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(key, forKey: .key) try container.encode(typedValue, forKey: .value) diff --git a/FlagsmithClient/Classes/TypedValue.swift b/FlagsmithClient/Classes/TypedValue.swift index 5bd5794..b97ef4f 100644 --- a/FlagsmithClient/Classes/TypedValue.swift +++ b/FlagsmithClient/Classes/TypedValue.swift @@ -17,7 +17,7 @@ public enum TypedValue: Equatable, Sendable { } extension TypedValue: Codable { - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() if let value = try? container.decode(Bool.self) { @@ -49,10 +49,10 @@ extension TypedValue: Codable { codingPath: [], debugDescription: "No decodable `TypedValue` value found." ) - throw DecodingError.valueNotFound(Decodable.self, context) + throw DecodingError.valueNotFound((any Decodable).self, context) } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() switch self { case .bool(let value): diff --git a/FlagsmithClient/Classes/UnknownTypeValue.swift b/FlagsmithClient/Classes/UnknownTypeValue.swift index 7ff2ed0..b8f01b6 100644 --- a/FlagsmithClient/Classes/UnknownTypeValue.swift +++ b/FlagsmithClient/Classes/UnknownTypeValue.swift @@ -15,7 +15,7 @@ public enum UnknownTypeValue: Decodable, Sendable { case int(Int), string(String), float(Float), null - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { if let int = try? decoder.singleValueContainer().decode(Int.self) { self = .int(int) return diff --git a/FlagsmithClient/Tests/APIManagerTests.swift b/FlagsmithClient/Tests/APIManagerTests.swift index 435e74f..083e2f8 100644 --- a/FlagsmithClient/Tests/APIManagerTests.swift +++ b/FlagsmithClient/Tests/APIManagerTests.swift @@ -17,23 +17,22 @@ final class APIManagerTests: FlagsmithClientTestCase { apiManager.apiKey = nil let requestFinished = expectation(description: "Request Finished") - var error: FlagsmithError? - apiManager.request(.getFlags) { (result: Result) in + apiManager.request(.getFlags) { (result: Result) in if case let .failure(e) = result { - error = e as? FlagsmithError + let error = e as? FlagsmithError + let flagsmithError = try! XCTUnwrap(error) + guard case .apiKey = flagsmithError else { + XCTFail("Wrong Error") + requestFinished.fulfill() + return + } } requestFinished.fulfill() } wait(for: [requestFinished], timeout: 1.0) - - let flagsmithError = try XCTUnwrap(error) - guard case .apiKey = flagsmithError else { - XCTFail("Wrong Error") - return - } } /// Verify that an invalid API url produces the expected error. @@ -42,40 +41,41 @@ final class APIManagerTests: FlagsmithClientTestCase { apiManager.baseURL = URL(fileURLWithPath: "/dev/null") let requestFinished = expectation(description: "Request Finished") - var error: FlagsmithError? - - apiManager.request(.getFlags) { (result: Result) in + + apiManager.request(.getFlags) { (result: Result) in if case let .failure(e) = result { - error = e as? FlagsmithError + let error = e as? FlagsmithError + let flagsmithError: FlagsmithError? = try! XCTUnwrap(error) + guard case .apiURL = flagsmithError else { + XCTFail("Wrong Error") + requestFinished.fulfill() + return + } } requestFinished.fulfill() } wait(for: [requestFinished], timeout: 1.0) - - let flagsmithError = try XCTUnwrap(error) - guard case .apiURL = flagsmithError else { - XCTFail("Wrong Error") - return - } + } - + func testConcurrentRequests() throws { apiManager.apiKey = "8D5ABC87-6BBF-4AE7-BC05-4DC1AFE770DF" let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent) var expectations:[XCTestExpectation] = []; let iterations = 500 - var error: FlagsmithError? for concurrentIteration in 1...iterations { let expectation = XCTestExpectation(description: "Multiple threads can access the APIManager \(concurrentIteration)") expectations.append(expectation) concurrentQueue.async { - self.apiManager.request(.getFlags) { (result: Result) in + self.apiManager.request(.getFlags) { (result: Result) in if case let .failure(e) = result { - error = e as? FlagsmithError + let error = e as? FlagsmithError + // Ensure that we didn't have any errors during the process + XCTAssertTrue(error == nil) } expectation.fulfill() } @@ -83,8 +83,6 @@ final class APIManagerTests: FlagsmithClientTestCase { } wait(for: expectations, timeout: 10) - // Ensure that we didn't have any errors during the process - XCTAssertTrue(error == nil) print("Finished!") } diff --git a/Package.swift b/Package.swift index 84a8f31..fc3cb06 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.9 import PackageDescription @@ -11,10 +11,14 @@ let package = Package( .target( name: "FlagsmithClient", dependencies: [], - path: "FlagsmithClient/Classes"), + path: "FlagsmithClient/Classes", + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=complete"), + .enableUpcomingFeature("ExistentialAny"), // https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md + ]), .testTarget( name: "FlagsmitClientTests", dependencies: ["FlagsmithClient"], - path: "FlagsmithClient/Tests"), + path: "FlagsmithClient/Tests") ] ) From b99cd7eef6b76b55c84887bc9f4b46fc2c3543b1 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Mon, 22 Apr 2024 15:28:26 +0100 Subject: [PATCH 06/10] Add support for privacy manifest for Cocoapods and SPM https://developer.apple.com/support/third-party-SDK-requirements/ --- Example/Podfile.lock | 2 +- .../FlagsmithClient.podspec.json | 5 + Example/Pods/Manifest.lock | 2 +- Example/Pods/Pods.xcodeproj/project.pbxproj | 426 +++++++++++------- ...agSmith_Privacy-FlagsmithClient-Info.plist | 24 + FlagsmithClient.podspec | 3 + FlagsmithClient/Assets/.gitkeep | 0 FlagsmithClient/Classes/PrivacyInfo.xcprivacy | 38 ++ Package.swift | 3 + 9 files changed, 341 insertions(+), 162 deletions(-) create mode 100644 Example/Pods/Target Support Files/FlagsmithClient/ResourceBundle-FlagSmith_Privacy-FlagsmithClient-Info.plist delete mode 100644 FlagsmithClient/Assets/.gitkeep create mode 100644 FlagsmithClient/Classes/PrivacyInfo.xcprivacy diff --git a/Example/Podfile.lock b/Example/Podfile.lock index d51e33c..75ff619 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -9,7 +9,7 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - FlagsmithClient: 3a96576f5a251c807e6aa0a3b0db55b3e1dfd0a3 + FlagsmithClient: 101151384696085c085d06b1c202946827e058d6 PODFILE CHECKSUM: 9fc876dee0cf031cae843156b0740a94b4994d8c diff --git a/Example/Pods/Local Podspecs/FlagsmithClient.podspec.json b/Example/Pods/Local Podspecs/FlagsmithClient.podspec.json index 02921d4..36cb165 100644 --- a/Example/Pods/Local Podspecs/FlagsmithClient.podspec.json +++ b/Example/Pods/Local Podspecs/FlagsmithClient.podspec.json @@ -15,6 +15,11 @@ "tag": "3.6.0" }, "social_media_url": "https://twitter.com/getflagsmith", + "resource_bundles": { + "FlagSmith_Privacy": [ + "Classes/PrivacyInfo.xcprivacy" + ] + }, "platforms": { "ios": "12.0" }, diff --git a/Example/Pods/Manifest.lock b/Example/Pods/Manifest.lock index d51e33c..75ff619 100644 --- a/Example/Pods/Manifest.lock +++ b/Example/Pods/Manifest.lock @@ -9,7 +9,7 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - FlagsmithClient: 3a96576f5a251c807e6aa0a3b0db55b3e1dfd0a3 + FlagsmithClient: 101151384696085c085d06b1c202946827e058d6 PODFILE CHECKSUM: 9fc876dee0cf031cae843156b0740a94b4994d8c diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index fd4ae47..07d636e 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -7,84 +7,96 @@ objects = { /* Begin PBXBuildFile section */ - 011196679F1DBAD78B3284BB60F1CC7E /* FlagsmithAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77743E9FB921729A0C34B2CBE7A1F4CD /* FlagsmithAnalytics.swift */; }; + 0083552669FD822D4A203F429BEF6F8C /* Flag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193A8DBBF59C95D3ADE40A6CACE88317 /* Flag.swift */; }; + 145BDA8A923A4A97F7D1985125F60E67 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */; }; + 1848829C29C5631D664CC0742A63EC33 /* APIManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 740BE3A9082C69EDDCE885472B239E2B /* APIManager.swift */; }; 1D8CF59EBBFF8148D15D0EFCAAA414A2 /* Pods-FlagsmithClient_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 02E4DC32B2FC713ED935462DA9F1CBF9 /* Pods-FlagsmithClient_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 28E2C1F327F2FEFC3D6193D0E775DD16 /* Flagsmith.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD71DED3359EE67BB9C921E2917DA7A /* Flagsmith.swift */; }; - 361AD17B75FDA9970FF500DEE9B88729 /* APIManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7482B233360C108341B85DF6F4F296D2 /* APIManager.swift */; }; + 200C93079A049CC69CF5844427F65359 /* TypedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E24C65212832E53CD320E489F86D56 /* TypedValue.swift */; }; + 36A99CA483CA4242032567EFEC9024E9 /* FlagsmithClient-FlagSmith_Privacy in Resources */ = {isa = PBXBuildFile; fileRef = 7F9C7D04F801E733A55ED8BA67AA84CE /* FlagsmithClient-FlagSmith_Privacy */; }; + 391C8F320DDD9EAB5996574CB69D38A6 /* Trait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F1CB950A2B9D94D55201B7B8191BB83 /* Trait.swift */; }; 3CB1981582BED249B73F39640A046EC6 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */; }; - 611EFA95BB13886C84B3692AF65E66F8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */; }; - 6EABC9E673A27FAC4180F2F08C4F2B3D /* CachedURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D88FCDAEE505ABAF4F0C9E59BEBB6A /* CachedURLResponse.swift */; }; - 7296A208906AA3BC74014294652982CA /* FlagsmithClient-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ECFF7C4FBE6AA88F7F7ECDCE6B66EA2 /* FlagsmithClient-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 78FC3BC1E0FADEB53A278A0A4C6DA6E8 /* UnknownTypeValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC513B1FDC4FEFFA6AC514C6DA6E3329 /* UnknownTypeValue.swift */; }; - 82EA3819E558968027E9A21E8E20A4A7 /* Flagsmith+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 919D3C18E297943C015F838AEEAD057F /* Flagsmith+Concurrency.swift */; }; - 90AE5624A1F9C4DAA5ED6ED01FB6AA98 /* Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D7B03B15ACDBBDA718D00CF6E18D416 /* Traits.swift */; }; - 95A960387ADF0D89DD60C4E6F563455F /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66035645C7F450B9A20A5AA2F1CC43B6 /* Router.swift */; }; - 97DE10D6905CEF731DD15EB738B843DD /* Trait.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB810BA1DD243ED447F96C1EC7AD63F /* Trait.swift */; }; - A3EAECF66E99CCF1D3251DB251AE6F9D /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED101D3AAE62D955200E49181AC69EF4 /* Identity.swift */; }; - A9DAAC7965A5F9D409F7335FC223BEE6 /* TypedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 158A52A6379630867E2111515A492DDE /* TypedValue.swift */; }; - B9ECC98F1865D64AA4B986208D0938C1 /* FlagsmithClient-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B880F3CB2ABE4DB1E22B3CC5541C2D8 /* FlagsmithClient-dummy.m */; }; + 41403B5BE8BE6F8EF3DD6D51EB8A05CD /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99036F2475C7DD87269B0693A1E508D1 /* Feature.swift */; }; + 4BC792BD210D6770E120952EC1A6149F /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560A2A43C2532F9A60D40C498910BDAD /* Identity.swift */; }; + 4FB62CFD8230DFD531423AC407FFBF12 /* CachedURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EDBFEFB26106F14D3ACD6A6E8DAF369 /* CachedURLResponse.swift */; }; + 7A6E9ACE7C0730D83DC68A952B450CBF /* FlagsmithError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 334A531B6E779CB5BBB111407DAB389B /* FlagsmithError.swift */; }; + 8C16371FF3BD75AA8903F1364B84C902 /* Flagsmith.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30C8727FF378667687B927B401ACFC2 /* Flagsmith.swift */; }; + 976897ECFF1B33FC921C0C50A0F2A156 /* Flagsmith+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33401B963FE6A82269201D80F422B5DC /* Flagsmith+Concurrency.swift */; }; + 9A443C01EC676628F306F7E210F63CBC /* Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD44276DCF6BDE68099A5762C3E3248 /* Traits.swift */; }; + C462C452A86C94C3B33BA403FD5102C3 /* FlagsmithAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010890F60854F25B5CF6D19BAF3B3774 /* FlagsmithAnalytics.swift */; }; C5344C505516395035EAB8B9FC111FE2 /* Pods-FlagsmithClient_Example-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F66D3EFFD8615AE149DCEEE155C049F /* Pods-FlagsmithClient_Example-dummy.m */; }; - CFC4FDB2F9D6E43C28FE902C6131C027 /* FlagsmithError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD829895C1256784784DBBFC3020A222 /* FlagsmithError.swift */; }; - CFFE0F58344636A5789C357F2BE50820 /* Feature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B77E851969A4CF4C7F75194BFC80A5B /* Feature.swift */; }; - F0F13B1D5F547C03C257DF46C49594E4 /* Flag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC8A87D5B1A7F57E12361CE1D5E95B8 /* Flag.swift */; }; + D804DCDEA2D5AC6CE809EC8D972B3A80 /* FlagsmithClient-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = CC0E8F8144A353EF4972DA8231507159 /* FlagsmithClient-dummy.m */; }; + E4162B22739D5F7D0728E35C4B806BB5 /* FlagsmithClient-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 42419C1B4BE086CC475FA111C3C7F89B /* FlagsmithClient-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F440E618661D6B6DB91C40AA067EE9AE /* UnknownTypeValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C37C353E63EE8DAA3130D043A0D2EB2F /* UnknownTypeValue.swift */; }; + F44ECBF1ABB58E1D4064C12D88BA909D /* PrivacyInfo.xcprivacy in Sources */ = {isa = PBXBuildFile; fileRef = E685A93BDD480D9EBB3E40D9D31E964E /* PrivacyInfo.xcprivacy */; }; + F7A953943973635341397277A7B77E44 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0332EE4AA77385BBB3C3A282AEFDFB /* Router.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - A39FC97A60B4E4EF1C00CDE9EDF2C25D /* PBXContainerItemProxy */ = { + 9D5732A5EB8BA218D9E058369413E3D9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 11AAFF8883A32F4C9C2E17C1B1AE0614; remoteInfo = FlagsmithClient; }; + AF57E5ECF5AA5CC6888F2C49B2406A20 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D089F624F760A848617C705876C78058; + remoteInfo = "FlagsmithClient-FlagSmith_Privacy"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 010890F60854F25B5CF6D19BAF3B3774 /* FlagsmithAnalytics.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FlagsmithAnalytics.swift; sourceTree = ""; }; + 01A052BA5448975C6D01FF1003C2FB1D /* FlagsmithClient-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "FlagsmithClient-Info.plist"; sourceTree = ""; }; + 01BED8F56D2D29829501F35ABA812D07 /* FlagsmithClient-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FlagsmithClient-prefix.pch"; sourceTree = ""; }; + 02B76A0CC5DD33DC3828D9A6C4BF09F0 /* FlagsmithClient.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = FlagsmithClient.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 02E4DC32B2FC713ED935462DA9F1CBF9 /* Pods-FlagsmithClient_Example-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-FlagsmithClient_Example-umbrella.h"; sourceTree = ""; }; - 0CD71DED3359EE67BB9C921E2917DA7A /* Flagsmith.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Flagsmith.swift; path = FlagsmithClient/Classes/Flagsmith.swift; sourceTree = ""; }; + 0F1CB950A2B9D94D55201B7B8191BB83 /* Trait.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Trait.swift; path = FlagsmithClient/Classes/Trait.swift; sourceTree = ""; }; 0F66D3EFFD8615AE149DCEEE155C049F /* Pods-FlagsmithClient_Example-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-FlagsmithClient_Example-dummy.m"; sourceTree = ""; }; - 0FAC90C1E63439160B035434A1CF25F1 /* FlagsmithClient-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "FlagsmithClient-Info.plist"; sourceTree = ""; }; - 158A52A6379630867E2111515A492DDE /* TypedValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TypedValue.swift; path = FlagsmithClient/Classes/TypedValue.swift; sourceTree = ""; }; + 193A8DBBF59C95D3ADE40A6CACE88317 /* Flag.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Flag.swift; path = FlagsmithClient/Classes/Flag.swift; sourceTree = ""; }; 250DE57229233B0BAD273A076F108A0E /* Pods-FlagsmithClient_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-FlagsmithClient_Example.release.xcconfig"; sourceTree = ""; }; - 2DC8A87D5B1A7F57E12361CE1D5E95B8 /* Flag.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Flag.swift; path = FlagsmithClient/Classes/Flag.swift; sourceTree = ""; }; + 33401B963FE6A82269201D80F422B5DC /* Flagsmith+Concurrency.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Flagsmith+Concurrency.swift"; path = "FlagsmithClient/Classes/Flagsmith+Concurrency.swift"; sourceTree = ""; }; + 334A531B6E779CB5BBB111407DAB389B /* FlagsmithError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FlagsmithError.swift; path = FlagsmithClient/Classes/FlagsmithError.swift; 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 = ""; }; - 47EFBF8F57718DBF9FE586CBDB6CC0DC /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE; sourceTree = ""; }; - 4CB810BA1DD243ED447F96C1EC7AD63F /* Trait.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Trait.swift; path = FlagsmithClient/Classes/Trait.swift; sourceTree = ""; }; - 591BEAEF977192FC1893BEAC10ABB7F0 /* FlagsmithClient.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = FlagsmithClient.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; - 5D7B03B15ACDBBDA718D00CF6E18D416 /* Traits.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Traits.swift; path = FlagsmithClient/Classes/Traits.swift; sourceTree = ""; }; - 66035645C7F450B9A20A5AA2F1CC43B6 /* Router.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; + 42419C1B4BE086CC475FA111C3C7F89B /* FlagsmithClient-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FlagsmithClient-umbrella.h"; sourceTree = ""; }; + 48E24C65212832E53CD320E489F86D56 /* TypedValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TypedValue.swift; path = FlagsmithClient/Classes/TypedValue.swift; sourceTree = ""; }; + 560A2A43C2532F9A60D40C498910BDAD /* Identity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Identity.swift; path = FlagsmithClient/Classes/Identity.swift; sourceTree = ""; }; + 5E0332EE4AA77385BBB3C3A282AEFDFB /* Router.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; + 5EDBFEFB26106F14D3ACD6A6E8DAF369 /* CachedURLResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CachedURLResponse.swift; sourceTree = ""; }; 68DDF334F75630BAA85571DF47D87C89 /* FlagsmithClient */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = FlagsmithClient; path = FlagsmithClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6B880F3CB2ABE4DB1E22B3CC5541C2D8 /* FlagsmithClient-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "FlagsmithClient-dummy.m"; sourceTree = ""; }; - 6DEAF76385A937740682012C42A3F628 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = ""; }; + 6A93708A2DFB73CD0A47844194EEEF0E /* FlagsmithClient.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = FlagsmithClient.debug.xcconfig; sourceTree = ""; }; + 6BD44276DCF6BDE68099A5762C3E3248 /* Traits.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Traits.swift; path = FlagsmithClient/Classes/Traits.swift; 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; }; - 7482B233360C108341B85DF6F4F296D2 /* APIManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = APIManager.swift; sourceTree = ""; }; - 77743E9FB921729A0C34B2CBE7A1F4CD /* FlagsmithAnalytics.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FlagsmithAnalytics.swift; sourceTree = ""; }; - 7ECFF7C4FBE6AA88F7F7ECDCE6B66EA2 /* FlagsmithClient-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FlagsmithClient-umbrella.h"; sourceTree = ""; }; - 8078BAE70CEB3897642FFDD030790F4B /* FlagsmithClient-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FlagsmithClient-prefix.pch"; sourceTree = ""; }; + 740BE3A9082C69EDDCE885472B239E2B /* APIManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = APIManager.swift; sourceTree = ""; }; + 79F99623D8F2206A889E3FC1BB7E028F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE; sourceTree = ""; }; + 7F9C7D04F801E733A55ED8BA67AA84CE /* FlagsmithClient-FlagSmith_Privacy */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = "FlagsmithClient-FlagSmith_Privacy"; path = FlagSmith_Privacy.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; 81A815D8A0C28062CD4A8224C6883D5D /* Pods-FlagsmithClient_Example.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-FlagsmithClient_Example.modulemap"; sourceTree = ""; }; - 8236C25B17E89C77288367CCF5B829A9 /* FlagsmithClient.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = FlagsmithClient.debug.xcconfig; sourceTree = ""; }; - 919D3C18E297943C015F838AEEAD057F /* Flagsmith+Concurrency.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Flagsmith+Concurrency.swift"; path = "FlagsmithClient/Classes/Flagsmith+Concurrency.swift"; sourceTree = ""; }; - 92D88FCDAEE505ABAF4F0C9E59BEBB6A /* CachedURLResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CachedURLResponse.swift; sourceTree = ""; }; - 9B77E851969A4CF4C7F75194BFC80A5B /* Feature.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Feature.swift; path = FlagsmithClient/Classes/Feature.swift; sourceTree = ""; }; + 97FECC05C71E791B8A6247E180CDC9BB /* FlagsmithClient.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = FlagsmithClient.modulemap; sourceTree = ""; }; + 99036F2475C7DD87269B0693A1E508D1 /* Feature.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Feature.swift; path = FlagsmithClient/Classes/Feature.swift; sourceTree = ""; }; 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 9E877EDD18BF267BC157C6CD6E133BA8 /* FlagsmithClient.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = FlagsmithClient.release.xcconfig; sourceTree = ""; }; A7D159AFD71C50F45CAAD458140D8648 /* Pods-FlagsmithClient_Example-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-FlagsmithClient_Example-acknowledgements.plist"; sourceTree = ""; }; - B27C6884FC1F031C5CCE03E04DA59B40 /* FlagsmithClient.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = FlagsmithClient.release.xcconfig; sourceTree = ""; }; - B4164394D814A1BA2FA6A7C7662493C5 /* FlagsmithClient.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = FlagsmithClient.modulemap; sourceTree = ""; }; + B3B60DD8AD34844A7BC951D1CADF34DB /* ResourceBundle-FlagSmith_Privacy-FlagsmithClient-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-FlagSmith_Privacy-FlagsmithClient-Info.plist"; sourceTree = ""; }; B7528E5D2516E37BD5B3A5DC02010477 /* Pods-FlagsmithClient_Example-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-FlagsmithClient_Example-frameworks.sh"; sourceTree = ""; }; C1817E8624F31BD483479898AD8A9F9C /* Pods-FlagsmithClient_Example */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-FlagsmithClient_Example"; path = Pods_FlagsmithClient_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - CD829895C1256784784DBBFC3020A222 /* FlagsmithError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FlagsmithError.swift; path = FlagsmithClient/Classes/FlagsmithError.swift; sourceTree = ""; }; - DC513B1FDC4FEFFA6AC514C6DA6E3329 /* UnknownTypeValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UnknownTypeValue.swift; path = FlagsmithClient/Classes/UnknownTypeValue.swift; sourceTree = ""; }; + C37C353E63EE8DAA3130D043A0D2EB2F /* UnknownTypeValue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UnknownTypeValue.swift; path = FlagsmithClient/Classes/UnknownTypeValue.swift; sourceTree = ""; }; + CC0E8F8144A353EF4972DA8231507159 /* FlagsmithClient-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "FlagsmithClient-dummy.m"; sourceTree = ""; }; + D30C8727FF378667687B927B401ACFC2 /* Flagsmith.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Flagsmith.swift; path = FlagsmithClient/Classes/Flagsmith.swift; sourceTree = ""; }; E28010F1C58E656FC37588C8A00FEE38 /* Pods-FlagsmithClient_Example-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-FlagsmithClient_Example-Info.plist"; sourceTree = ""; }; - ED101D3AAE62D955200E49181AC69EF4 /* Identity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Identity.swift; path = FlagsmithClient/Classes/Identity.swift; sourceTree = ""; }; + E685A93BDD480D9EBB3E40D9D31E964E /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = FlagsmithClient/Classes/PrivacyInfo.xcprivacy; sourceTree = ""; }; + F68F491C2EB4793199539D7BB50E1A2B /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 5978E19B2CF28713F5F5FAE1384AF5B4 /* Frameworks */ = { + 12C0163E057E9D8C44B9E1980454B186 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 611EFA95BB13886C84B3692AF65E66F8 /* Foundation.framework in Frameworks */, + 145BDA8A923A4A97F7D1985125F60E67 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -96,31 +108,36 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E4C1A32CB959CCA3FE667491F71AA361 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0A0ADE663140E20C1071E72A619CA4E1 /* Products */ = { + 004E68B560F9B8386F5DB36906B270A5 /* Products */ = { isa = PBXGroup; children = ( 68DDF334F75630BAA85571DF47D87C89 /* FlagsmithClient */, + 7F9C7D04F801E733A55ED8BA67AA84CE /* FlagsmithClient-FlagSmith_Privacy */, C1817E8624F31BD483479898AD8A9F9C /* Pods-FlagsmithClient_Example */, ); name = Products; sourceTree = ""; }; - 27EC56B1B7D07F4EDD85D29A44A2225F /* Support Files */ = { + 5504E831FC9EDDEE6663BD8753EC1AC0 /* Internal */ = { isa = PBXGroup; children = ( - B4164394D814A1BA2FA6A7C7662493C5 /* FlagsmithClient.modulemap */, - 6B880F3CB2ABE4DB1E22B3CC5541C2D8 /* FlagsmithClient-dummy.m */, - 0FAC90C1E63439160B035434A1CF25F1 /* FlagsmithClient-Info.plist */, - 8078BAE70CEB3897642FFDD030790F4B /* FlagsmithClient-prefix.pch */, - 7ECFF7C4FBE6AA88F7F7ECDCE6B66EA2 /* FlagsmithClient-umbrella.h */, - 8236C25B17E89C77288367CCF5B829A9 /* FlagsmithClient.debug.xcconfig */, - B27C6884FC1F031C5CCE03E04DA59B40 /* FlagsmithClient.release.xcconfig */, + 740BE3A9082C69EDDCE885472B239E2B /* APIManager.swift */, + 5EDBFEFB26106F14D3ACD6A6E8DAF369 /* CachedURLResponse.swift */, + 010890F60854F25B5CF6D19BAF3B3774 /* FlagsmithAnalytics.swift */, + 5E0332EE4AA77385BBB3C3A282AEFDFB /* Router.swift */, ); - name = "Support Files"; - path = "Example/Pods/Target Support Files/FlagsmithClient"; + name = Internal; + path = FlagsmithClient/Classes/Internal; sourceTree = ""; }; 578452D2E740E91742655AC8F1636D1F /* iOS */ = { @@ -148,33 +165,22 @@ path = "Target Support Files/Pods-FlagsmithClient_Example"; sourceTree = ""; }; - 743FF37545C268C8B74711C93A390E32 /* FlagsmithClient */ = { + 83DF8963328A9E0494B7C83B809AF479 /* Development Pods */ = { isa = PBXGroup; children = ( - 9B77E851969A4CF4C7F75194BFC80A5B /* Feature.swift */, - 2DC8A87D5B1A7F57E12361CE1D5E95B8 /* Flag.swift */, - 0CD71DED3359EE67BB9C921E2917DA7A /* Flagsmith.swift */, - 919D3C18E297943C015F838AEEAD057F /* Flagsmith+Concurrency.swift */, - CD829895C1256784784DBBFC3020A222 /* FlagsmithError.swift */, - ED101D3AAE62D955200E49181AC69EF4 /* Identity.swift */, - 4CB810BA1DD243ED447F96C1EC7AD63F /* Trait.swift */, - 5D7B03B15ACDBBDA718D00CF6E18D416 /* Traits.swift */, - 158A52A6379630867E2111515A492DDE /* TypedValue.swift */, - DC513B1FDC4FEFFA6AC514C6DA6E3329 /* UnknownTypeValue.swift */, - CE24075F0AA68AF97EBFE976DF0970CD /* Internal */, - E3A6070C6F3B520898B030B46D84A64C /* Pod */, - 27EC56B1B7D07F4EDD85D29A44A2225F /* Support Files */, + EB5B366C3102D180EE97EBB112A2C98A /* FlagsmithClient */, ); - name = FlagsmithClient; - path = ../..; + name = "Development Pods"; sourceTree = ""; }; - 8771110EE67E622B18478ADE12D92BE5 /* Development Pods */ = { + A880B46BBE672F8B5031ACED24CDDC51 /* Pod */ = { isa = PBXGroup; children = ( - 743FF37545C268C8B74711C93A390E32 /* FlagsmithClient */, + 02B76A0CC5DD33DC3828D9A6C4BF09F0 /* FlagsmithClient.podspec */, + 79F99623D8F2206A889E3FC1BB7E028F /* LICENSE */, + F68F491C2EB4793199539D7BB50E1A2B /* README.md */, ); - name = "Development Pods"; + name = Pod; sourceTree = ""; }; BA8A3B9AA623A3EC5D887B56C04C7D73 /* Targets Support Files */ = { @@ -185,25 +191,13 @@ name = "Targets Support Files"; sourceTree = ""; }; - CE24075F0AA68AF97EBFE976DF0970CD /* Internal */ = { - isa = PBXGroup; - children = ( - 7482B233360C108341B85DF6F4F296D2 /* APIManager.swift */, - 92D88FCDAEE505ABAF4F0C9E59BEBB6A /* CachedURLResponse.swift */, - 77743E9FB921729A0C34B2CBE7A1F4CD /* FlagsmithAnalytics.swift */, - 66035645C7F450B9A20A5AA2F1CC43B6 /* Router.swift */, - ); - name = Internal; - path = FlagsmithClient/Classes/Internal; - sourceTree = ""; - }; CF1408CF629C7361332E53B88F7BD30C = { isa = PBXGroup; children = ( 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, - 8771110EE67E622B18478ADE12D92BE5 /* Development Pods */, + 83DF8963328A9E0494B7C83B809AF479 /* Development Pods */, D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */, - 0A0ADE663140E20C1071E72A619CA4E1 /* Products */, + 004E68B560F9B8386F5DB36906B270A5 /* Products */, BA8A3B9AA623A3EC5D887B56C04C7D73 /* Targets Support Files */, ); sourceTree = ""; @@ -216,14 +210,42 @@ name = Frameworks; sourceTree = ""; }; - E3A6070C6F3B520898B030B46D84A64C /* Pod */ = { + DD874B272A864A7F34C517E2F9F6438B /* Support Files */ = { isa = PBXGroup; children = ( - 591BEAEF977192FC1893BEAC10ABB7F0 /* FlagsmithClient.podspec */, - 47EFBF8F57718DBF9FE586CBDB6CC0DC /* LICENSE */, - 6DEAF76385A937740682012C42A3F628 /* README.md */, + 97FECC05C71E791B8A6247E180CDC9BB /* FlagsmithClient.modulemap */, + CC0E8F8144A353EF4972DA8231507159 /* FlagsmithClient-dummy.m */, + 01A052BA5448975C6D01FF1003C2FB1D /* FlagsmithClient-Info.plist */, + 01BED8F56D2D29829501F35ABA812D07 /* FlagsmithClient-prefix.pch */, + 42419C1B4BE086CC475FA111C3C7F89B /* FlagsmithClient-umbrella.h */, + 6A93708A2DFB73CD0A47844194EEEF0E /* FlagsmithClient.debug.xcconfig */, + 9E877EDD18BF267BC157C6CD6E133BA8 /* FlagsmithClient.release.xcconfig */, + B3B60DD8AD34844A7BC951D1CADF34DB /* ResourceBundle-FlagSmith_Privacy-FlagsmithClient-Info.plist */, ); - name = Pod; + name = "Support Files"; + path = "Example/Pods/Target Support Files/FlagsmithClient"; + sourceTree = ""; + }; + EB5B366C3102D180EE97EBB112A2C98A /* FlagsmithClient */ = { + isa = PBXGroup; + children = ( + 99036F2475C7DD87269B0693A1E508D1 /* Feature.swift */, + 193A8DBBF59C95D3ADE40A6CACE88317 /* Flag.swift */, + D30C8727FF378667687B927B401ACFC2 /* Flagsmith.swift */, + 33401B963FE6A82269201D80F422B5DC /* Flagsmith+Concurrency.swift */, + 334A531B6E779CB5BBB111407DAB389B /* FlagsmithError.swift */, + 560A2A43C2532F9A60D40C498910BDAD /* Identity.swift */, + E685A93BDD480D9EBB3E40D9D31E964E /* PrivacyInfo.xcprivacy */, + 0F1CB950A2B9D94D55201B7B8191BB83 /* Trait.swift */, + 6BD44276DCF6BDE68099A5762C3E3248 /* Traits.swift */, + 48E24C65212832E53CD320E489F86D56 /* TypedValue.swift */, + C37C353E63EE8DAA3130D043A0D2EB2F /* UnknownTypeValue.swift */, + 5504E831FC9EDDEE6663BD8753EC1AC0 /* Internal */, + A880B46BBE672F8B5031ACED24CDDC51 /* Pod */, + DD874B272A864A7F34C517E2F9F6438B /* Support Files */, + ); + name = FlagsmithClient; + path = ../..; sourceTree = ""; }; /* End PBXGroup section */ @@ -237,11 +259,11 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 98DBC9670D86FAE13772BFFB393D0029 /* Headers */ = { + B0B82A0BFE945677F027568CF40F606B /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 7296A208906AA3BC74014294652982CA /* FlagsmithClient-umbrella.h in Headers */, + E4162B22739D5F7D0728E35C4B806BB5 /* FlagsmithClient-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -250,16 +272,17 @@ /* Begin PBXNativeTarget section */ 11AAFF8883A32F4C9C2E17C1B1AE0614 /* FlagsmithClient */ = { isa = PBXNativeTarget; - buildConfigurationList = CE8A22033473608C91867F497BC5A2CD /* Build configuration list for PBXNativeTarget "FlagsmithClient" */; + buildConfigurationList = ABD7FC62C2B9ACFD025EB9237D4ADBFE /* Build configuration list for PBXNativeTarget "FlagsmithClient" */; buildPhases = ( - 98DBC9670D86FAE13772BFFB393D0029 /* Headers */, - 29A741F17A46BDD22AB2213A04FDE505 /* Sources */, - 5978E19B2CF28713F5F5FAE1384AF5B4 /* Frameworks */, - D0516BC3E528E09D1C43539B158EED35 /* Resources */, + B0B82A0BFE945677F027568CF40F606B /* Headers */, + D9978955D1F7D97BA5C53A6F2B02A6AF /* Sources */, + 12C0163E057E9D8C44B9E1980454B186 /* Frameworks */, + 4B831344AB43BCEBB55AC7A0D1BD1D0A /* Resources */, ); buildRules = ( ); dependencies = ( + AB585B4EA3B1A537EA9707B6BF2CECDC /* PBXTargetDependency */, ); name = FlagsmithClient; productName = FlagsmithClient; @@ -278,13 +301,30 @@ buildRules = ( ); dependencies = ( - 6A2A4CDB21FA43CDB219D90367EBBD10 /* PBXTargetDependency */, + 54A900312AEF2FB8D47E285B59D9AF7F /* PBXTargetDependency */, ); name = "Pods-FlagsmithClient_Example"; productName = Pods_FlagsmithClient_Example; productReference = C1817E8624F31BD483479898AD8A9F9C /* Pods-FlagsmithClient_Example */; productType = "com.apple.product-type.framework"; }; + D089F624F760A848617C705876C78058 /* FlagsmithClient-FlagSmith_Privacy */ = { + isa = PBXNativeTarget; + buildConfigurationList = D9D115EFB53F932E6EDDE9FCD80E521A /* Build configuration list for PBXNativeTarget "FlagsmithClient-FlagSmith_Privacy" */; + buildPhases = ( + A4E89541AF518F781354C01097A6F67B /* Sources */, + E4C1A32CB959CCA3FE667491F71AA361 /* Frameworks */, + EE871D842E8E68C7D392182D72D98F65 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "FlagsmithClient-FlagSmith_Privacy"; + productName = FlagSmith_Privacy; + productReference = 7F9C7D04F801E733A55ED8BA67AA84CE /* FlagsmithClient-FlagSmith_Privacy */; + productType = "com.apple.product-type.bundle"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -303,17 +343,26 @@ en, ); mainGroup = CF1408CF629C7361332E53B88F7BD30C; - productRefGroup = 0A0ADE663140E20C1071E72A619CA4E1 /* Products */; + productRefGroup = 004E68B560F9B8386F5DB36906B270A5 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 11AAFF8883A32F4C9C2E17C1B1AE0614 /* FlagsmithClient */, + D089F624F760A848617C705876C78058 /* FlagsmithClient-FlagSmith_Privacy */, 770799B5DCFAB0B1BCF1BD365E2C1BC5 /* Pods-FlagsmithClient_Example */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 4B831344AB43BCEBB55AC7A0D1BD1D0A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 36A99CA483CA4242032567EFEC9024E9 /* FlagsmithClient-FlagSmith_Privacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 7DD4986852439651139309DE21E874D5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -321,7 +370,7 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D0516BC3E528E09D1C43539B158EED35 /* Resources */ = { + EE871D842E8E68C7D392182D72D98F65 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -331,48 +380,96 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 29A741F17A46BDD22AB2213A04FDE505 /* Sources */ = { + 660F00CEBC7EEE195DEDCC6CBB923CEA /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 361AD17B75FDA9970FF500DEE9B88729 /* APIManager.swift in Sources */, - 6EABC9E673A27FAC4180F2F08C4F2B3D /* CachedURLResponse.swift in Sources */, - CFFE0F58344636A5789C357F2BE50820 /* Feature.swift in Sources */, - F0F13B1D5F547C03C257DF46C49594E4 /* Flag.swift in Sources */, - 28E2C1F327F2FEFC3D6193D0E775DD16 /* Flagsmith.swift in Sources */, - 82EA3819E558968027E9A21E8E20A4A7 /* Flagsmith+Concurrency.swift in Sources */, - 011196679F1DBAD78B3284BB60F1CC7E /* FlagsmithAnalytics.swift in Sources */, - B9ECC98F1865D64AA4B986208D0938C1 /* FlagsmithClient-dummy.m in Sources */, - CFC4FDB2F9D6E43C28FE902C6131C027 /* FlagsmithError.swift in Sources */, - A3EAECF66E99CCF1D3251DB251AE6F9D /* Identity.swift in Sources */, - 95A960387ADF0D89DD60C4E6F563455F /* Router.swift in Sources */, - 97DE10D6905CEF731DD15EB738B843DD /* Trait.swift in Sources */, - 90AE5624A1F9C4DAA5ED6ED01FB6AA98 /* Traits.swift in Sources */, - A9DAAC7965A5F9D409F7335FC223BEE6 /* TypedValue.swift in Sources */, - 78FC3BC1E0FADEB53A278A0A4C6DA6E8 /* UnknownTypeValue.swift in Sources */, + C5344C505516395035EAB8B9FC111FE2 /* Pods-FlagsmithClient_Example-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 660F00CEBC7EEE195DEDCC6CBB923CEA /* Sources */ = { + A4E89541AF518F781354C01097A6F67B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C5344C505516395035EAB8B9FC111FE2 /* Pods-FlagsmithClient_Example-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D9978955D1F7D97BA5C53A6F2B02A6AF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1848829C29C5631D664CC0742A63EC33 /* APIManager.swift in Sources */, + 4FB62CFD8230DFD531423AC407FFBF12 /* CachedURLResponse.swift in Sources */, + 41403B5BE8BE6F8EF3DD6D51EB8A05CD /* Feature.swift in Sources */, + 0083552669FD822D4A203F429BEF6F8C /* Flag.swift in Sources */, + 8C16371FF3BD75AA8903F1364B84C902 /* Flagsmith.swift in Sources */, + 976897ECFF1B33FC921C0C50A0F2A156 /* Flagsmith+Concurrency.swift in Sources */, + C462C452A86C94C3B33BA403FD5102C3 /* FlagsmithAnalytics.swift in Sources */, + D804DCDEA2D5AC6CE809EC8D972B3A80 /* FlagsmithClient-dummy.m in Sources */, + 7A6E9ACE7C0730D83DC68A952B450CBF /* FlagsmithError.swift in Sources */, + 4BC792BD210D6770E120952EC1A6149F /* Identity.swift in Sources */, + F44ECBF1ABB58E1D4064C12D88BA909D /* PrivacyInfo.xcprivacy in Sources */, + F7A953943973635341397277A7B77E44 /* Router.swift in Sources */, + 391C8F320DDD9EAB5996574CB69D38A6 /* Trait.swift in Sources */, + 9A443C01EC676628F306F7E210F63CBC /* Traits.swift in Sources */, + 200C93079A049CC69CF5844427F65359 /* TypedValue.swift in Sources */, + F440E618661D6B6DB91C40AA067EE9AE /* UnknownTypeValue.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 6A2A4CDB21FA43CDB219D90367EBBD10 /* PBXTargetDependency */ = { + 54A900312AEF2FB8D47E285B59D9AF7F /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = FlagsmithClient; target = 11AAFF8883A32F4C9C2E17C1B1AE0614 /* FlagsmithClient */; - targetProxy = A39FC97A60B4E4EF1C00CDE9EDF2C25D /* PBXContainerItemProxy */; + targetProxy = 9D5732A5EB8BA218D9E058369413E3D9 /* PBXContainerItemProxy */; + }; + AB585B4EA3B1A537EA9707B6BF2CECDC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "FlagsmithClient-FlagSmith_Privacy"; + target = D089F624F760A848617C705876C78058 /* FlagsmithClient-FlagSmith_Privacy */; + targetProxy = AF57E5ECF5AA5CC6888F2C49B2406A20 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 049EEB1E25BB61D149D6D294514FA82A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6A93708A2DFB73CD0A47844194EEEF0E /* FlagsmithClient.debug.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/FlagsmithClient"; + IBSC_MODULE = FlagsmithClient; + INFOPLIST_FILE = "Target Support Files/FlagsmithClient/ResourceBundle-FlagSmith_Privacy-FlagsmithClient-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + PRODUCT_NAME = FlagSmith_Privacy; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Debug; + }; + 2769E86984A0005886AC6DA3DEEF5E58 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9E877EDD18BF267BC157C6CD6E133BA8 /* FlagsmithClient.release.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/FlagsmithClient"; + IBSC_MODULE = FlagsmithClient; + INFOPLIST_FILE = "Target Support Files/FlagsmithClient/ResourceBundle-FlagSmith_Privacy-FlagsmithClient-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + PRODUCT_NAME = FlagSmith_Privacy; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; 2B9E26EAE2CD392AD762421F663075A1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -439,9 +536,9 @@ }; name = Debug; }; - 2FEE03729C8CD1FA45680EB6B9941372 /* Release */ = { + 58E625716FA7C3CCECC76379D437F52F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B27C6884FC1F031C5CCE03E04DA59B40 /* FlagsmithClient.release.xcconfig */; + baseConfigurationReference = 9E877EDD18BF267BC157C6CD6E133BA8 /* FlagsmithClient.release.xcconfig */; buildSettings = { ARCHS = "$(ARCHS_STANDARD_64_BIT)"; CLANG_ENABLE_OBJC_WEAK = NO; @@ -472,6 +569,38 @@ }; name = Release; }; + 599DF1132B45CDE985F883321D83D83F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6A93708A2DFB73CD0A47844194EEEF0E /* FlagsmithClient.debug.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/FlagsmithClient/FlagsmithClient-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient.modulemap"; + PRODUCT_MODULE_NAME = FlagsmithClient; + PRODUCT_NAME = FlagsmithClient; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.6; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; 63FAF33E1C55B71A5F5A8B3CC8749F99 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -568,38 +697,6 @@ }; name = Debug; }; - BDFC8E9E764D7F6D61D71A36E95488F3 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 8236C25B17E89C77288367CCF5B829A9 /* FlagsmithClient.debug.xcconfig */; - buildSettings = { - ARCHS = "$(ARCHS_STANDARD_64_BIT)"; - CLANG_ENABLE_OBJC_WEAK = NO; - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/FlagsmithClient/FlagsmithClient-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MODULEMAP_FILE = "Target Support Files/FlagsmithClient/FlagsmithClient.modulemap"; - PRODUCT_MODULE_NAME = FlagsmithClient; - PRODUCT_NAME = FlagsmithClient; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 5.6; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; D22AD683643CD18DDDA1624DDA6590F4 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 250DE57229233B0BAD273A076F108A0E /* Pods-FlagsmithClient_Example.release.xcconfig */; @@ -656,11 +753,20 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - CE8A22033473608C91867F497BC5A2CD /* Build configuration list for PBXNativeTarget "FlagsmithClient" */ = { + ABD7FC62C2B9ACFD025EB9237D4ADBFE /* Build configuration list for PBXNativeTarget "FlagsmithClient" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 599DF1132B45CDE985F883321D83D83F /* Debug */, + 58E625716FA7C3CCECC76379D437F52F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D9D115EFB53F932E6EDDE9FCD80E521A /* Build configuration list for PBXNativeTarget "FlagsmithClient-FlagSmith_Privacy" */ = { isa = XCConfigurationList; buildConfigurations = ( - BDFC8E9E764D7F6D61D71A36E95488F3 /* Debug */, - 2FEE03729C8CD1FA45680EB6B9941372 /* Release */, + 049EEB1E25BB61D149D6D294514FA82A /* Debug */, + 2769E86984A0005886AC6DA3DEEF5E58 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Example/Pods/Target Support Files/FlagsmithClient/ResourceBundle-FlagSmith_Privacy-FlagsmithClient-Info.plist b/Example/Pods/Target Support Files/FlagsmithClient/ResourceBundle-FlagSmith_Privacy-FlagsmithClient-Info.plist new file mode 100644 index 0000000..832e218 --- /dev/null +++ b/Example/Pods/Target Support Files/FlagsmithClient/ResourceBundle-FlagSmith_Privacy-FlagsmithClient-Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + BNDL + CFBundleShortVersionString + 3.6.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/FlagsmithClient.podspec b/FlagsmithClient.podspec index a657ef4..21e7787 100644 --- a/FlagsmithClient.podspec +++ b/FlagsmithClient.podspec @@ -15,6 +15,9 @@ Pod::Spec.new do |s| s.author = { 'Kyle Johnson' => 'Kyle.johnson@flagsmith.com' } s.source = { :git => 'https://github.com/Flagsmith/flagsmith-ios-client.git', :tag => s.version.to_s } s.social_media_url = 'https://twitter.com/getflagsmith' + s.resource_bundles = { + 'FlagSmith_Privacy' => ['Classes/PrivacyInfo.xcprivacy'], + } s.ios.deployment_target = '12.0' diff --git a/FlagsmithClient/Assets/.gitkeep b/FlagsmithClient/Assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/FlagsmithClient/Classes/PrivacyInfo.xcprivacy b/FlagsmithClient/Classes/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..3df1117 --- /dev/null +++ b/FlagsmithClient/Classes/PrivacyInfo.xcprivacy @@ -0,0 +1,38 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyTrackingDomains + + edge.api.flagsmith.com + + NSPrivacyTracking + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + + + + diff --git a/Package.swift b/Package.swift index fc3cb06..e1a812d 100644 --- a/Package.swift +++ b/Package.swift @@ -12,6 +12,9 @@ let package = Package( name: "FlagsmithClient", dependencies: [], path: "FlagsmithClient/Classes", + resources: [ + .copy("PrivacyInfo.xcprivacy"), + ], swiftSettings: [ .enableExperimentalFeature("StrictConcurrency=complete"), .enableUpcomingFeature("ExistentialAny"), // https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md From 6e05ea89e8d7f3d3c14f2af44c0a522683a4f333 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Mon, 22 Apr 2024 15:29:12 +0100 Subject: [PATCH 07/10] Add qos: parameter to our DispatchQueues to avoid console errors starting the SDK --- FlagsmithClient/Classes/Internal/APIManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FlagsmithClient/Classes/Internal/APIManager.swift b/FlagsmithClient/Classes/Internal/APIManager.swift index d5bb506..52ea8d2 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -53,8 +53,8 @@ final class APIManager : NSObject, URLSessionDataDelegate, @unchecked Sendable { // store the completion handlers and accumulated data for each task private var tasksToCompletionHandlers:[Int: @Sendable(Result) -> Void] = [:] private var tasksToData:[Int:Data] = [:] - private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue") - let propertiesSerialAccessQueue = DispatchQueue(label: "propertiesSerialAccessQueue") + private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue", qos: .default) + let propertiesSerialAccessQueue = DispatchQueue(label: "propertiesSerialAccessQueue", qos: .default) override init() { super.init() From 51be65a88ab055fcc3011046ef7ee700a6168a20 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Mon, 22 Apr 2024 15:29:41 +0100 Subject: [PATCH 08/10] Also add qos to the test app --- Example/FlagsmithClient/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/FlagsmithClient/AppDelegate.swift b/Example/FlagsmithClient/AppDelegate.swift index a68b1d0..b147a0d 100644 --- a/Example/FlagsmithClient/AppDelegate.swift +++ b/Example/FlagsmithClient/AppDelegate.swift @@ -17,7 +17,7 @@ func isSuccess(_ result: Result) -> Bool { class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent) + let concurrentQueue = DispatchQueue(label: "concurrentQueue", qos: .default, attributes: .concurrent) func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. From 55b22093ae6fbe6ceb99e922f1dc5d64493e6461 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Tue, 23 Apr 2024 08:54:49 +0100 Subject: [PATCH 09/10] Update to the latest macOS and the latest macOS (12) currently for the runners to check everything is fine --- .github/workflows/pull-request.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c964181..998f522 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -8,7 +8,11 @@ on: jobs: macos-build: - runs-on: macOS-13 + # macOS-latest images are not the most recent + # The macos-latest workflow label currently uses the macOS 12 runner image + # The vast majority of macOS developers would be using the latest version of macOS + # Current list here: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#choosing-github-hosted-runners + runs-on: macOS-14 steps: - uses: actions/checkout@v4 @@ -17,8 +21,19 @@ jobs: - name: Run tests run: swift test -v - ubuntu-build: + macos-build-latest: + # Let's also check that the code builds on the 'latest' macOS version according to Microsoft + # At 23rd April 2023 the latest macOS version is macOS 12 + runs-on: macOS-latest + steps: + - uses: actions/checkout@v4 + - name: Build (macOS) + run: swift build -v + - name: Run tests + run: swift test -v + + ubuntu-build: runs-on: ubuntu-latest steps: From a0d1f08f7940c0a0cc00ee1337f9ac14cabf96b8 Mon Sep 17 00:00:00 2001 From: Gareth Reese Date: Tue, 23 Apr 2024 08:58:42 +0100 Subject: [PATCH 10/10] Let's build on 13 and 14 as 12 is too old --- .github/workflows/pull-request.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 998f522..af5d298 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -7,9 +7,9 @@ on: branches: [main] jobs: - macos-build: + macos-build-14: # macOS-latest images are not the most recent - # The macos-latest workflow label currently uses the macOS 12 runner image + # The macos-latest workflow label currently uses the macOS 12 runner image, which doesn't include the build-tools we need # The vast majority of macOS developers would be using the latest version of macOS # Current list here: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#choosing-github-hosted-runners runs-on: macOS-14 @@ -21,10 +21,10 @@ jobs: - name: Run tests run: swift test -v - macos-build-latest: - # Let's also check that the code builds on the 'latest' macOS version according to Microsoft - # At 23rd April 2023 the latest macOS version is macOS 12 - runs-on: macOS-latest + macos-build-13: + # Let's also check that the code builds on macOS 13 + # At 23rd April 2023 the 'latest' macOS version is macOS 12 + runs-on: macOS-13 steps: - uses: actions/checkout@v4