Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
78d4205
Latest updates to the caching tests, still need to validate that this…
gazreese Sep 15, 2025
52078dd
Tidied up and got what looks like a decent set of tests. Will test ag…
gazreese Sep 15, 2025
c9a3180
Update the Example UI for testing the SDK more easily
gazreese Sep 15, 2025
7ff413b
Ensure we're not pushing an API key
gazreese Sep 15, 2025
91ce986
Fix a crash with the SSE functionality where the completion handler w…
gazreese Sep 15, 2025
4254dbe
Update the testing documentation
gazreese Sep 15, 2025
5968ff9
Fix up some lints
gazreese Sep 15, 2025
8d0b167
Check out what the swift version is in the tests and fix an error
gazreese Sep 15, 2025
ff60586
Solidify the TTL tests and make sure we skip properly if we don't hav…
gazreese Sep 15, 2025
66594a7
Try with a matrix test to get the tests running
gazreese Sep 15, 2025
bf5c8ff
properly validating
gazreese Sep 15, 2025
baeddb4
Updates to the tests to target the commonly used versions
gazreese Sep 15, 2025
832ee23
Missing version on setup-swift
gazreese Sep 15, 2025
26c1cac
Try the 'next' branch on the workflow to see it things are any better
gazreese Sep 15, 2025
0123ace
Try using the old method and xcrun
gazreese Sep 15, 2025
500867e
Check for API key and get rid of the extraneous runs
gazreese Sep 15, 2025
a27ffc0
Test mostly running fine in Xcode, will work through the CustomerCach…
gazreese Sep 16, 2025
551e41e
Updates to the test to make them more stringent with loss of API key
gazreese Sep 16, 2025
9d32672
Remove some debug logging
gazreese Sep 16, 2025
c9ec285
Lint line length error
gazreese Sep 16, 2025
04aab47
Tidy up the tests, still failing on the concurrent test
gazreese Sep 16, 2025
53fe239
Added the session configuration to serial access queue
gazreese Sep 17, 2025
98aad17
Some more updates to the CacheTests, hopefully this is it
gazreese Sep 17, 2025
02169d2
Resolve some of the threading issues we were seeing in Swift 6.1 and …
gazreese Sep 17, 2025
1cb0740
Add missing env passthrough in the action
gazreese Sep 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 22 additions & 31 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,39 @@ on:
branches: [main]

jobs:
macos-build-14:
macos-latest:
# 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@v4
- name: Build (macOS)
run: swift build -v
- name: Run tests
run: swift test -v

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

runs-on: macOS-latest
steps:
- uses: actions/checkout@v4
- name: Check for FLAGSMITH_TEST_API_KEY
run: |
if [ -z "$FLAGSMITH_TEST_API_KEY" ]; then
echo "Warning: FLAGSMITH_TEST_API_KEY environment variable is not set"
exit 1
fi
env:
FLAGSMITH_TEST_API_KEY: ${{ secrets.FLAGSMITH_TEST_API_KEY }}
- name: Build (macOS)
run: swift build -v
- name: Run tests
run: swift test -v

ubuntu-build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Build (Ubuntu)
run: swift build -v
run: swift --version && swift build
- name: Run tests
run: swift test -v

run: swift test
env:
FLAGSMITH_TEST_API_KEY: ${{ secrets.FLAGSMITH_TEST_API_KEY }}
swift-lint:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Check for FLAGSMITH_TEST_API_KEY
run: |
if [ -z "$FLAGSMITH_TEST_API_KEY" ]; then
echo "Warning: FLAGSMITH_TEST_API_KEY environment variable is not set"
exit 1
fi
env:
FLAGSMITH_TEST_API_KEY: ${{ secrets.FLAGSMITH_TEST_API_KEY }}

- name: Run SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ Carthage/Build
# This was causing confusion on PRs and is really just a record of the last time the example
# pods were re-built from scratch. It's not useful to keep in the repo.
Example/Podfile.lock

# Test configuration with real API keys (do not commit)
FlagsmithClient/Tests/test-config.json
139 changes: 139 additions & 0 deletions AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// AppDelegate.swift
// FlagsmithClient
//
// Created by Tomash Tsiupiak on 06/20/2019.
// Copyright (c) 2019 Tomash Tsiupiak. All rights reserved.
//

import UIKit
import FlagsmithClient
#if canImport(SwiftUI)
import SwiftUI
#endif

func isSuccess<T, F>(_ result: Result<T, F>) -> Bool {
if case .success = result { return true } else { return false }
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?
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.
Flagsmith.shared.apiKey = "<add your API key from the Flagsmith dashboard here>"

// set default flags
Flagsmith.shared.defaultFlags = [Flag(featureName: "feature_a", enabled: false),
Flag(featureName: "font_size", intValue: 12, enabled: true),
Flag(featureName: "my_name", stringValue: "Testing", enabled: true)]

// set cache on / off (defaults to off)
Flagsmith.shared.cacheConfig.useCache = true

// set custom cache to use (defaults to shared URLCache)
// Flagsmith.shared.cacheConfig.cache = <CUSTOM_CACHE>

// set skip API on / off (defaults to off)
Flagsmith.shared.cacheConfig.skipAPI = false

// set cache TTL in seconds (defaults to 0, i.e. infinite)
Flagsmith.shared.cacheConfig.cacheTTL = 90

// set analytics on or off
Flagsmith.shared.enableAnalytics = true

// Enable real time updates
Flagsmith.shared.enableRealtimeUpdates = true

// set the analytics flush period in seconds
Flagsmith.shared.analyticsFlushPeriod = 10

Flagsmith.shared.getFeatureFlags { (result) in
print(result)
}
Flagsmith.shared.hasFeatureFlag(withID: "freeze_delinquent_accounts") { (result) in
print(result)
}

// Try getting the feature flags concurrently to ensure that this does not cause any issues
// This was originally highlighted in https://github.com/Flagsmith/flagsmith-ios-client/pull/40
for _ in 1...20 {
concurrentQueue.async {
Flagsmith.shared.getFeatureFlags { (_) in
}
}
}

// Flagsmith.shared.setTrait(Trait(key: "<my_key>", value: "<my_value>"), forIdentity: "<my_identity>") { (result) in print(result) }
// Flagsmith.shared.getIdentity("<my_key>") { (result) in print(result) }

// Launch SwiftUIView
if #available(iOS 13.0, *) {
let swiftUIView = SwiftUIView()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UIHostingController(rootView: swiftUIView)
window?.makeKeyAndVisible()
}
Comment on lines +75 to +81
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard SwiftUI usage with canImport to prevent build failures on toolchains without SwiftUI.

UIHostingController is referenced outside the #if canImport(SwiftUI) guard. Wrap usage too.

-    // Launch SwiftUIView
-    if #available(iOS 13.0, *) {
-      let swiftUIView = SwiftUIView()
-      window = UIWindow(frame: UIScreen.main.bounds)
-      window?.rootViewController = UIHostingController(rootView: swiftUIView)
-      window?.makeKeyAndVisible()
-    }
+    // Launch SwiftUIView
+    #if canImport(SwiftUI)
+    if #available(iOS 13.0, *) {
+      let swiftUIView = SwiftUIView()
+      window = UIWindow(frame: UIScreen.main.bounds)
+      window?.rootViewController = UIHostingController(rootView: swiftUIView)
+      window?.makeKeyAndVisible()
+    }
+    #endif
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Launch SwiftUIView
if #available(iOS 13.0, *) {
let swiftUIView = SwiftUIView()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UIHostingController(rootView: swiftUIView)
window?.makeKeyAndVisible()
}
// Launch SwiftUIView
#if canImport(SwiftUI)
if #available(iOS 13.0, *) {
let swiftUIView = SwiftUIView()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UIHostingController(rootView: swiftUIView)
window?.makeKeyAndVisible()
}
#endif
🤖 Prompt for AI Agents
In AppDelegate.swift around lines 75–81, the SwiftUI usage references
UIHostingController outside of a canImport(SwiftUI) guard which can break builds
on toolchains without SwiftUI; wrap the SwiftUIView, UIHostingController and
related UIWindow code in a conditional compilation block using #if
canImport(SwiftUI) (and import SwiftUI inside it), keeping the existing
@available(iOS 13.0, *) runtime check nested as needed, and provide an
alternative (or leave blank) for toolchains that cannot import SwiftUI so the
file compiles everywhere.


return true
}

func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}

func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}

func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

#if swift(>=5.5.2)
/// (Example) Setup the app based on the available feature flags.
///
/// **Flagsmith** supports the Swift Concurrency feature `async`/`await`.
/// Requests and logic can be handled in a streamlined order,
/// eliminating the need to nest multiple completion handlers.
@available(iOS 13.0, *)
func determineAppConfiguration() async {
let flagsmith = Flagsmith.shared

do {
if try await flagsmith.hasFeatureFlag(withID: "ab_test_enabled") {
if let theme = try await flagsmith.getValueForFeature(withID: "app_theme") {
setTheme(theme)
} else {
let flags = try await flagsmith.getFeatureFlags()
processFlags(flags)
}
} else {
let trait = Trait(key: "selected_tint_color", value: "orange")
let identity = "4DDBFBCA-3B6E-4C59-B107-954F84FD7F6D"
try await flagsmith.setTrait(trait, forIdentity: identity)
}
} catch {
print(error)
}
}

func setTheme(_ theme: TypedValue) {}
func processFlags(_ flags: [Flag]) {}
#endif
}
28 changes: 20 additions & 8 deletions Example/FlagsmithClient.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objectVersion = 77;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -143,14 +143,14 @@
};
};
buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "FlagsmithClient" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 607FACC71AFB9204008FA782;
preferredProjectObjectVersion = 77;
productRefGroup = 607FACD11AFB9204008FA782 /* Products */;
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -201,13 +201,16 @@
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-FlagsmithClient_Example/Pods-FlagsmithClient_Example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
Comment on lines +204 to +206
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Switch to filelists for Embed Pods frameworks is good; ensure filelists exist.

Using xcfilelists reduces merge noise. Verify the Debug/Release filelist files are present under Target Support Files.

Also applies to: 210-212


🏁 Script executed:

#!/bin/bash
fd -H --strip-cwd-prefix 'Pods-FlagsmithClient_Example-frameworks-*-(input|output)-files.xcfilelist' \
  'Example/Pods/Target Support Files/Pods-FlagsmithClient_Example'

Length of output: 166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking for xcfilelist files for Pods-FlagsmithClient_Example..."

paths=(
  "Example/Pods/Target Support Files/Pods-FlagsmithClient_Example"
  "Pods/Target Support Files/Pods-FlagsmithClient_Example"
)

found=0
for p in "${paths[@]}"; do
  if [ -d "$p" ]; then
    echo "Directory exists: $p"
    echo "Listing .xcfilelist files (maxdepth 1):"
    find "$p" -maxdepth 1 -type f -iname "*.xcfilelist" -print -exec ls -l {} \; || true

    echo "Listing files matching frameworks input/output patterns:"
    find "$p" -maxdepth 1 -type f \( -iname "Pods-FlagsmithClient_Example-frameworks-*-input-files.xcfilelist" -o -iname "Pods-FlagsmithClient_Example-frameworks-*-output-files.xcfilelist" \) -print -exec ls -l {} \; || true

    found=1
  else
    echo "Directory not found: $p"
  fi
done

echo "Also searching the whole repo for any .xcfilelist files (will print matches):"
find . -type f -iname "*.xcfilelist" -print -exec ls -l {} \; || true

if [ "$found" -eq 0 ]; then
  echo "No Target Support Files dirs found for Pods-FlagsmithClient_Example. Showing Example/ and Pods/ top-level directories for context:"
  echo "---- Example/ ----"
  ls -la Example || true
  echo "---- Pods/ ----"
  ls -la Pods || true
fi

Length of output: 397


Missing .xcfilelist files referenced by project

Example/FlagsmithClient.xcodeproj/project.pbxproj references ${PODS_ROOT}/Target Support Files/Pods-FlagsmithClient_Example/Pods-FlagsmithClient_Example-frameworks-${CONFIGURATION}-input-files.xcfilelist (lines 204–206; also applies to 210–212), but Example/Pods/Target Support Files/Pods-FlagsmithClient_Example contains no .xcfilelist files.

  • Action: either add the generated Debug/Release input/output .xcfilelist files (e.g., Pods-FlagsmithClient_Example-frameworks-Debug-input-files.xcfilelist and corresponding output files — typically created by running pod install) and commit them, or remove/replace the inputFileListPaths entries in the Xcode project so the build does not reference missing files.
🤖 Prompt for AI Agents
In Example/FlagsmithClient.xcodeproj/project.pbxproj around lines 204–206 (and
similarly 210–212) the project references missing .xcfilelist files under
${PODS_ROOT}/Target Support Files/Pods-FlagsmithClient_Example; fix by either
(A) regenerating and committing the Pod-generated input/output .xcfilelist files
(run pod install in Example to produce
Pods-FlagsmithClient_Example-frameworks-{Debug,Release}-{input,output}-files.xcfilelist
and add them to the repo), or (B) remove or replace the inputFileListPaths
entries in the pbxproj so the build phases do not reference non-existent files
(update the project file to point to valid files or remove the file list paths),
then verify the Xcode build succeeds.

inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-FlagsmithClient_Example/Pods-FlagsmithClient_Example-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/FlagsmithClient/FlagsmithClient.framework",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-FlagsmithClient_Example/Pods-FlagsmithClient_Example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FlagsmithClient.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
Expand Down Expand Up @@ -350,7 +353,8 @@
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
Expand All @@ -362,7 +366,11 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = RYCT86V4LM;
INFOPLIST_FILE = FlagsmithClient/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -377,7 +385,11 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = RYCT86V4LM;
INFOPLIST_FILE = FlagsmithClient/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MODULE_NAME = ExampleApp;
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
2 changes: 1 addition & 1 deletion Example/FlagsmithClient/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
Flagsmith.shared.apiKey = "<add your API key from the Flagsmith settings page>"
Flagsmith.shared.apiKey = "<add your API key from the Flagsmith dashboard here>"

// set default flags
Flagsmith.shared.defaultFlags = [Flag(featureName: "feature_a", enabled: false),
Expand Down
Loading
Loading