diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index d14dd42..08d19aa 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -7,22 +7,37 @@ on: branches: [main] jobs: - macos-build: - runs-on: macos-latest + macos-build-14: + # macOS-latest images are not the most recent + # 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 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Build (macOS) run: swift build -v - name: Run tests run: swift test -v - ubuntu-build: + 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 + - name: Build (macOS) + run: swift build -v + - name: Run tests + run: swift test -v + + ubuntu-build: 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/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/FlagsmithClient/AppDelegate.swift b/Example/FlagsmithClient/AppDelegate.swift index 16c2daa..af5f90d 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 { 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..73b84da 100644 --- a/FlagsmithClient.podspec +++ b/FlagsmithClient.podspec @@ -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 diff --git a/FlagsmithClient/Classes/Feature.swift b/FlagsmithClient/Classes/Feature.swift index f4618c9..2f1baa6 100644 --- a/FlagsmithClient/Classes/Feature.swift +++ b/FlagsmithClient/Classes/Feature.swift @@ -22,7 +22,7 @@ public struct Feature: Codable, Sendable { public let type: String? public let description: String? - 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(name, forKey: .name) try container.encodeIfPresent(type, forKey: .type) diff --git a/FlagsmithClient/Classes/Flag.swift b/FlagsmithClient/Classes/Flag.swift index 2a9d792..4c009be 100644 --- a/FlagsmithClient/Classes/Flag.swift +++ b/FlagsmithClient/Classes/Flag.swift @@ -64,7 +64,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(feature, forKey: .feature) try container.encode(value, forKey: .value) diff --git a/FlagsmithClient/Classes/Flagsmith.swift b/FlagsmithClient/Classes/Flagsmith.swift index 28331d7..7a1a967 100644 --- a/FlagsmithClient/Classes/Flagsmith.swift +++ b/FlagsmithClient/Classes/Flagsmith.swift @@ -12,11 +12,11 @@ import Foundation /// Manage feature flags and remote config across multiple projects, /// environments and organisations. -public class Flagsmith { +public final class Flagsmith: @unchecked Sendable { /// Shared singleton client object - public static let shared = Flagsmith() - private let apiManager = APIManager() - private lazy var analytics = FlagsmithAnalytics(apiManager: apiManager) + public static let shared: Flagsmith = .init() + private let apiManager: APIManager + private let analytics: FlagsmithAnalytics /// Base URL /// @@ -47,12 +47,35 @@ public class Flagsmith { } /// Default flags to fall back on if an API call fails - public var defaultFlags: [Flag] = [] + private var _defaultFlags: [Flag] = [] + public var defaultFlags: [Flag] { + get { + apiManager.propertiesSerialAccessQueue.sync { _defaultFlags } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _defaultFlags = newValue + } + } + } /// Configuration class for the cache settings - public var cacheConfig: CacheConfig = .init() + private var _cacheConfig: CacheConfig = .init() + public var cacheConfig: CacheConfig { + get { + apiManager.propertiesSerialAccessQueue.sync { _cacheConfig } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _cacheConfig = newValue + } + } + } - private init() {} + private init() { + apiManager = APIManager() + analytics = FlagsmithAnalytics(apiManager: apiManager) + } /// Get all feature flags (flags and remote config) optionally for a specific identity /// @@ -60,7 +83,7 @@ public class Flagsmith { /// - 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) + completion: @Sendable @escaping (Result<[Flag], any Error>) -> Void) { if let identity = identity { getIdentity(identity) { result in @@ -99,7 +122,7 @@ public class Flagsmith { /// - 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) + completion: @Sendable @escaping (Result) -> Void) { analytics.trackEvent(flagName: id) getFeatureFlags(forIdentity: identity) { result in @@ -126,7 +149,7 @@ public class Flagsmith { @available(*, deprecated, renamed: "getValueForFeature(withID:forIdentity:completion:)") public func getFeatureValue(withID id: String, forIdentity identity: String? = nil, - completion: @escaping (Result) -> Void) + completion: @Sendable @escaping (Result) -> Void) { analytics.trackEvent(flagName: id) getFeatureFlags(forIdentity: identity) { result in @@ -152,7 +175,7 @@ public class Flagsmith { /// - 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) + completion: @Sendable @escaping (Result) -> Void) { analytics.trackEvent(flagName: id) getFeatureFlags(forIdentity: identity) { result in @@ -178,7 +201,7 @@ public class Flagsmith { /// - 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) + completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) { getIdentity(identity) { result in switch result { @@ -203,7 +226,7 @@ public class Flagsmith { /// - 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) + completion: @Sendable @escaping (Result) -> Void) { getIdentity(identity) { result in switch result { @@ -224,7 +247,7 @@ public class Flagsmith { /// - 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) + completion: @Sendable @escaping (Result) -> Void) { apiManager.request(.postTrait(trait: trait, identity: identity)) { (result: Result) in completion(result) @@ -239,7 +262,7 @@ public class Flagsmith { /// - 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) + completion: @Sendable @escaping (Result<[Trait], any Error>) -> Void) { apiManager.request(.postTraits(identity: identity, traits: traits)) { (result: Result) in completion(result.map(\.traits)) @@ -252,7 +275,7 @@ public class Flagsmith { /// - 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) + completion: @Sendable @escaping (Result) -> Void) { apiManager.request(.getIdentity(identity: identity)) { (result: Result) in completion(result) @@ -265,7 +288,7 @@ public class Flagsmith { } } -public class CacheConfig { +public final class CacheConfig { /// Cache to use when enabled, defaults to the shared app cache public var cache: URLCache = .shared diff --git a/FlagsmithClient/Classes/FlagsmithError.swift b/FlagsmithClient/Classes/FlagsmithError.swift index 4f60711..2500e66 100644 --- a/FlagsmithClient/Classes/FlagsmithError.swift +++ b/FlagsmithClient/Classes/FlagsmithError.swift @@ -20,7 +20,7 @@ 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 { @@ -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 7ae1052..67589ec 100644 --- a/FlagsmithClient/Classes/Internal/APIManager.swift +++ b/FlagsmithClient/Classes/Internal/APIManager.swift @@ -11,18 +11,50 @@ import Foundation #endif /// Handles interaction with a **Flagsmith** api. -class APIManager: NSObject, URLSessionDataDelegate { - private var session: URLSession! +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 + } + } + } /// Base `URL` used for requests. - var baseURL = URL(string: "https://edge.api.flagsmith.com/api/v1/")! + private var _baseURL = URL(string: "https://edge.api.flagsmith.com/api/v1/")! + var baseURL: URL { + get { + propertiesSerialAccessQueue.sync { _baseURL } + } + set { + propertiesSerialAccessQueue.sync { + _baseURL = newValue + } + } + } + /// API Key unique to an organization. - var apiKey: String? + private var _apiKey: String? + var apiKey: String? { + get { + propertiesSerialAccessQueue.sync { _apiKey } + } + set { + propertiesSerialAccessQueue.sync { + _apiKey = newValue + } + } + } // store the completion handlers and accumulated data for each task - private var tasksToCompletionHandlers: [Int: (Result) -> Void] = [:] + private var tasksToCompletionHandlers: [Int: @Sendable (Result) -> Void] = [:] private var tasksToData: [Int: Data] = [:] - private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue") + private let serialAccessQueue = DispatchQueue(label: "flagsmithSerialAccessQueue", qos: .default) + let propertiesSerialAccessQueue = DispatchQueue(label: "propertiesSerialAccessQueue", qos: .default) override init() { super.init() @@ -30,7 +62,7 @@ class APIManager: NSObject, URLSessionDataDelegate { session = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main) } - func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { serialAccessQueue.sync { if let dataTask = task as? URLSessionDataTask { if let completion = tasksToCompletionHandlers[dataTask.taskIdentifier] { @@ -48,7 +80,7 @@ class APIManager: NSObject, URLSessionDataDelegate { } func urlSession(_: URLSession, dataTask _: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, - completionHandler: @escaping (CachedURLResponse?) -> Void) + completionHandler: @Sendable @escaping (CachedURLResponse?) -> Void) { serialAccessQueue.sync { // intercept and modify the cache settings for the response @@ -81,7 +113,7 @@ class APIManager: NSObject, URLSessionDataDelegate { /// - 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) { + private func request(_ router: Router, completion: @Sendable @escaping (Result) -> Void) { guard let apiKey = apiKey, !apiKey.isEmpty else { completion(.failure(FlagsmithError.apiKey)) return @@ -118,7 +150,7 @@ class APIManager: NSObject, URLSessionDataDelegate { /// - 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) { + func request(_ router: Router, completion: @Sendable @escaping (Result) -> Void) { request(router) { (result: Result) in switch result { case let .failure(error): @@ -136,7 +168,7 @@ class APIManager: NSObject, URLSessionDataDelegate { /// - 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) + completion: @Sendable @escaping (Result) -> Void) { request(router) { (result: Result) in switch result { diff --git a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift index cbe03e8..e5ed65d 100644 --- a/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift +++ b/FlagsmithClient/Classes/Internal/FlagsmithAnalytics.swift @@ -8,21 +8,59 @@ import Foundation /// Internal analytics for the **FlagsmithClient** -class FlagsmithAnalytics { +final class FlagsmithAnalytics: @unchecked Sendable { /// Indicates if analytics are enabled. - var enableAnalytics: Bool = true + 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 = 10 { - didSet { + var flushPeriod: Int { + get { + apiManager.propertiesSerialAccessQueue.sync { _flushPeriod } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _flushPeriod = newValue + } setupTimer() } } private unowned let apiManager: APIManager - private let eventsKey = "events" - private var events: [String: Int] = [:] - private var timer: Timer? + private var _events: [String: Int] = [:] + private var events: [String: Int] { + get { + apiManager.propertiesSerialAccessQueue.sync { _events } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _events = newValue + } + } + } + + private var _timer: Timer? + private var timer: Timer? { + get { + apiManager.propertiesSerialAccessQueue.sync { _timer } + } + set { + apiManager.propertiesSerialAccessQueue.sync { + _timer = newValue + } + } + } init(apiManager: APIManager) { self.apiManager = apiManager @@ -88,7 +126,7 @@ class FlagsmithAnalytics { return } - apiManager.request(.postAnalytics(events: events)) { [weak self] (result: Result) in + apiManager.request(.postAnalytics(events: events)) { @Sendable [weak self] (result: Result) in switch result { case .failure: print("Upload analytics failed") diff --git a/FlagsmithClient/Classes/Trait.swift b/FlagsmithClient/Classes/Trait.swift index 8195915..837e710 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 bb6c223..41cd04b 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 let .bool(value): diff --git a/FlagsmithClient/Classes/UnknownTypeValue.swift b/FlagsmithClient/Classes/UnknownTypeValue.swift index aebbb50..bf8efa9 100644 --- a/FlagsmithClient/Classes/UnknownTypeValue.swift +++ b/FlagsmithClient/Classes/UnknownTypeValue.swift @@ -14,7 +14,7 @@ import Foundation 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 c87a39f..bbd5c71 100644 --- a/FlagsmithClient/Tests/APIManagerTests.swift +++ b/FlagsmithClient/Tests/APIManagerTests.swift @@ -16,23 +16,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(err) = result { - error = err as? FlagsmithError + let error = err as? FlagsmithError + + guard let flagsmithError = try? XCTUnwrap(error), 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. @@ -41,23 +40,22 @@ 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(err) = result { - error = err as? FlagsmithError + let error = err as? FlagsmithError + let flagsmithError: FlagsmithError? = try? XCTUnwrap(error) + guard let flagsmithError = flagsmithError, 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 { @@ -66,15 +64,16 @@ final class APIManagerTests: FlagsmithClientTestCase { 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(err) = result { - error = err as? FlagsmithError + let error = err as? FlagsmithError + // Ensure that we didn't have any errors during the process + XCTAssertTrue(error == nil) } expectation.fulfill() } @@ -82,8 +81,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 f2c4445..933395e 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.9 import PackageDescription @@ -15,10 +15,11 @@ let package = Package( .target( name: "FlagsmithClient", dependencies: [], - path: "FlagsmithClient/Classes" - // plugins: [ - // .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")] - ), + 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"],