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"],