Swift (iOS)
This SDK supports the following platforms and versions:
- iOS 12 and above
- Apple TvOS 12 and above
- Apple WatchOS 5.0 and above
Technical Specifications
Platform Support
The GrowthBook Swift SDK provides comprehensive support for all Apple platforms:
- iOS: 12.0+ (iPhone, iPad)
- macOS: 10.15+ (Catalina and later)
- tvOS: 12.0+ (Apple TV)
- watchOS: 5.0+ (Apple Watch)
- visionOS: 1.0+ (Apple Vision Pro)
Performance Characteristics
- Lightweight: Minimal memory footprint and fast initialization
- Offline-First: Works without network connectivity using cached features
- Efficient Caching: Intelligent cache management with automatic cleanup
- Background Sync: Real-time updates via Server-Sent Events (SSE)
- Low Latency: Sub-millisecond feature evaluation performance
Implementation Details
Architecture
- Native Swift: Built entirely in Swift for optimal performance
- Protocol-Oriented: Uses Swift protocols for extensibility and testability
- Memory Safe: Leverages Swift's memory safety features
- Concurrent: Thread-safe implementation with proper synchronization
Dependencies
- No External Dependencies: Zero third-party dependencies for maximum reliability
- Foundation Framework: Uses only Apple's Foundation framework
- CryptoKit: Leverages Apple's CryptoKit for encryption operations
- Network Framework: Uses modern URLSession for networking
Data Formats
- JSON: Native JSON parsing and serialization
- Base64: Standard Base64 encoding for encrypted payloads
- UTF-8: Full Unicode support for international applications
Network Protocol
- HTTPS: Secure communication with GrowthBook servers
- REST API: Standard HTTP/HTTPS for feature fetching
- SSE: Server-Sent Events for real-time updates
- Retry Logic: Exponential backoff for failed requests
Memory Management
- ARC: Automatic Reference Counting for memory management
- Weak References: Prevents retain cycles in callbacks
- Lazy Loading: Features are loaded on-demand to minimize memory usage
- Cache Limits: Configurable cache size limits to prevent memory bloat
Thread Safety
- Concurrent Access: Safe for use across multiple threads
- Atomic Operations: Thread-safe attribute updates and feature evaluation
- Queue Management: Proper dispatch queue usage for background operations
- Lock-Free: Minimal locking for optimal performance
Installation
CocoaPods
Add the following to your podfile:
source 'https://github.com/CocoaPods/Specs.git'
target 'MyApp' do
pod 'GrowthBook-IOS'
end
Then, install:
pod install
Swift Package Manager (SPM)
Add GrowthBook to your Package.swift
file:
dependencies: [
.package(url: "https://github.com/growthbook/growthbook-swift.git")
]
Quick Start
1) Configure
var gb: GrowthBookSDK = GrowthBookBuilder(
// Your GrowthBook API host
apiHost: "https://cdn.growthbook.io",
// Your client key
clientKey: "sdk-abc123",
// Optional: encryption key if your features are encrypted
encryptionKey: "abcdef98765",
// Required user attributes for targeting (can be empty initially)
attributes: [:],
// Required: called whenever someone is put into an experiment
trackingCallback: { experiment, result in
// Track experiment exposures in your analytics
},
// Optional: real-time updates via SSE
backgroundSync: false,
// Optional: called when features are refreshed (SSE or manual refresh)
refreshHandler: { success in
// Update UI or trigger re-render if needed
}
).initializer()
2) Set attributes
var attrs = [
"id": "12345", // stable user id for bucketing
"deviceId": "abc123", // fallback identifier if user id is unavailable
"loggedIn": true,
"country": "US"
]
gb.setAttributes(attrs)
3) Start feature flagging
if gb.isOn(feature: "feature-usage-code") {
// Feature is enabled!
}
import Foundation // For JSON type
let value = gb.getFeatureValue(feature: "button-color", default: JSON("blue"))
4) Wait for features to load
- With background sync off, call
gb.refreshCache()
at app start or navigation points and show a loading state until complete. - With background sync on, initial cache loads synchronously if present and updates stream in via SSE.
- Use
refreshHandler
to re-render when new features arrive.
var gb: GrowthBookSDK = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: [:],
trackingCallback: { experiment, result in
// Track experiments
},
// Callback when features are refreshed
refreshHandler: { success in
if success {
print("Features refreshed successfully")
} else {
print("Failed to refresh features")
}
},
backgroundSync: true
).initializer()
Loading Features
Built-in fetching and refresh mechanisms
If you pass an apiHost
and clientKey
into GrowthBookBuilder
, the SDK handles network requests, caching, retry/backoff, and decryption (when encryptionKey
is provided). Use gb.refreshCache()
to manually refresh on navigation or app resume.
var gb: GrowthBookSDK = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: [:],
trackingCallback: { _, _ in },
// Optional: decrypt encrypted payloads
encryptionKey: "abcdef98765",
// Optional: real-time updates can be enabled later
backgroundSync: false
).initializer()
// Refresh on demand (e.g., app start or navigation)
_ = gb.refreshCache()
Secure requests: custom HTTP headers
Add custom headers for API requests (e.g., auth tokens, custom user-agent). Provide default headers for the standard feature fetches and separate headers for SSE streaming if needed.
var gb: GrowthBookSDK = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: [:],
trackingCallback: { _, _ in }
)
// Sent with feature fetch requests to the apiHost
.setApiHostRequestHeaders([
"Authorization": "Bearer <token>",
"X-App-Version": "1.2.3"
])
// Sent with SSE connections to the streaming host
.setStreamingHostRequestHeaders([
"Authorization": "Bearer <token>",
"X-Device": "iPhone15,3"
])
.initializer()
Streaming updates (SSE)
Enable backgroundSync: true
to receive real-time updates via SSE. The SDK reconnects automatically when network conditions change. Use refreshHandler
to update UI when features change.
var gb: GrowthBookSDK = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: [:],
trackingCallback: { _, _ in },
refreshHandler: { success in
// Trigger UI updates when features change
},
backgroundSync: true
).initializer()
Monitor the SSE lifecycle using your app logs/telemetry. On transient network errors, the SDK will attempt automatic reconnects with backoff.
SSE resume support (Last-Event-Id)
When using backgroundSync: true
, the SDK connects via SSE. To resume after interruptions without duplicating events, you can provide the last received event id so the server continues from that point.
// Persist and restore lastEventId yourself (e.g., UserDefaults) if you need fine-grained control
let lastEventId = UserDefaults.standard.string(forKey: "gb_last_event_id")
var gb: GrowthBookSDK = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: [:],
trackingCallback: { _, _ in },
backgroundSync: true
)
.setStreamingHostRequestHeaders(["Last-Event-Id": lastEventId ?? ""]) // optional
.initializer()
// Somewhere in your SSE handler, update lastEventId when new events arrive
// UserDefaults.standard.set(newEventId, forKey: "gb_last_event_id")
Caching
The SDK persists downloaded feature payloads and related metadata to disk. By default, it will:
- Use a platform-specific system directory
- iOS/watchOS/macOS: Application Support (
.applicationSupport
) - tvOS: Caches (
.caches
), since Application Support is not available on tvOS
- iOS/watchOS/macOS: Application Support (
- Create the cache folder automatically on first use
- Isolate data per
clientKey
by nesting under a client-specific subfolder - Generate deterministic cache file names by hashing keys with SHA256 and prefixing file names with the first 5 characters of the hash for readability
You can fully customize where and how the SDK caches data:
// Cache configuration
var gb: GrowthBookSDK = GrowthBookBuilder(...)
// Choose a system directory (defaults shown below)
.setSystemCacheDirectory(
// Default: .applicationSupport for iOS/watchOS/macOS; .caches for tvOS
#if os(tvOS)
.caches
#else
.applicationSupport
#endif
)
// Or, provide an absolute custom path. The SDK will create it if missing.
.setCustomCacheDirectory("/path/to/custom/cache")
.initializer()
// Cache management
// Removes all on-disk cache for this SDK instance (scoped to the configured client key)
gb.clearCache()
- The cache directory will be created automatically if it does not exist.
- Cache file keys are SHA256-hashed with a 5-character prefix for human-friendly diagnostics while avoiding path-length and special-character issues.
- Multiple environments/apps can safely share the same parent directory; per-
clientKey
subfolders prevent collisions.
Cache Key Hashing
The SDK uses SHA-256 for generating deterministic cache file names:
- Purpose: Create unique, collision-resistant cache file identifiers
- Format: First 5 characters of SHA-256 hash for human-readable diagnostics
- Isolation: Per-client-key subfolders prevent cross-environment collisions
Custom integration (local evaluation)
If you prefer to control network and caching yourself, pass features as a JSON Data
blob into the builder. This enables fully local evaluation and works offline-first.
// Create JSON data for features
let featuresJSON = """
{
"feature1": { "defaultValue": true }
}
"""
let featuresData = featuresJSON.data(using: .utf8)!
// Initialize with features data (local evaluation)
var gb: GrowthBookSDK = GrowthBookBuilder(
features: featuresData,
attributes: [:],
trackingCallback: { _, _ in },
backgroundSync: false
).initializer()
Remote evaluation vs. local evaluation
- Local evaluation loads a full feature payload into the SDK (e.g., via built-in fetching or a bundled JSON) and evaluates rules on-device.
- Remote evaluation delegates evaluation to GrowthBook and downloads already-evaluated feature values for the current attributes. This reduces payload size and can simplify privacy controls.
- Choose based on privacy, payload size, and offline requirements.
Local evaluation (bundled JSON):
let featuresJSON = """{ "feature1": { "defaultValue": true } }"""
let featuresData = featuresJSON.data(using: .utf8)!
let gbLocal = GrowthBookBuilder(
features: featuresData,
attributes: ["id": "12345"],
trackingCallback: { _, _ in }
).initializer()
Remote evaluation (standard fetching):
let gbRemote = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: ["id": "12345"],
trackingCallback: { _, _ in }
).initializer()
Manual remote evaluation
If you use remote evaluation mode, you can trigger a re-fetch of evaluated payloads when user attributes or context change without waiting for the standard refresh cadence.
// When user attributes change significantly, trigger a remote eval refresh
gb.setAttributes(["id": "user_123", "plan": "pro"])
gb.refreshForRemoteEval()
Custom payload structure:
When using remote evaluation, ensure the attributes you send include all fields required by your targeting rules. You may also include metadata your edge or proxy expects (e.g., headers added via setApiHostRequestHeaders
).
// Attributes control what the remote evaluator returns
gb.setAttributes([
"id": "user_123",
"country": "US",
"appVersion": "1.3.0",
"plan": "pro"
])
// Then manually trigger remote evaluation
gb.refreshForRemoteEval()
Encrypted features
The Swift SDK uses AES-CBC (Advanced Encryption Standard - Cipher Block Chaining) encryption for securing feature payloads:
- Algorithm: AES-CBC with 128-bit key length
- Key Format: Base64-encoded encryption key
- IV (Initialization Vector): 16-byte random IV generated for each encryption operation
- Payload Format:
{base64_iv}.{base64_encrypted_data}
GrowthBook uses Server-Side Encryption to encrypt feature payloads using AES-CBC before transmission. The Swift SDK automatically decrypts payloads when the encryptionKey
is provided in the initialization. See below example:
let gbEncrypted = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
encryptionKey: "abcdef98765",
attributes: [:],
trackingCallback: { _, _ in }
).initializer()
Decrypted features are cached securely on the device. If decryption fails, the SDK gracefully falls back to default feature values.
While encryption adds a layer of security, remember that:
- The decryption key is accessible within the client-side application
- For highly sensitive information, consider using Remote Evaluation instead
- Encryption keys should be managed securely and rotated regularly
Sticky bucketing
Sticky bucketing ensures that once a user is assigned to a variation in an experiment or feature rule configured as “Sticky,” they will continue to see the same variation on subsequent evaluations. Read more about Sticky bucketing Feature
How to enable:
- In GrowthBook, enable Sticky bucketing on the experiment or on the feature rule.
- In the SDK, provide a stable identity attribute (commonly
id
). If noid
is available (e.g., logged out), use a device-scoped fallback such asdeviceId
until login.
By default, when a sticky rule runs the SDK will:
- Look up an existing sticky assignment for the current identity key
- If found, return the same variation without re-assigning
- If not found, evaluate the rule normally, persist the chosen variation locally, and return it
- Persist across app restarts and feature refreshes; changing non-identity attributes (e.g.,
country
) will not change the sticky result. Changing the identity (e.g., user logs in/out) causes a lookup under the new identity key
gb.setAttributes([
"id": "user_123", // stable across sessions
"deviceId": "abcd-efgh" // fallback when logged out
])
Custom Sticky Bucket Service with custom storage
Use this to control how and where sticky assignments are persisted. Provide a prefix to namespace data (e.g., by app or environment) and a storage backend that implements simple key/value get-set-delete semantics (e.g., UserDefaults
, Keychain, SQLite, or your own cache layer). If not provided, the SDK uses its built-in persistent storage to remember sticky assignments.
// Custom Sticky Bucket Service with custom storage
let customStorage = MyCachingManager()
let stickyService = StickyBucketService(
prefix: "myapp_sticky_",
localStorage: customStorage
)
var gb: GrowthBookSDK = GrowthBookBuilder(...)
.setStickyBucketService(stickyBucketService: stickyService)
.initializer()
Tips:
- Use a unique
prefix
per environment (e.g.,prod_
,staging_
) to avoid cross-environment bleed. - To “forget” a user’s sticky assignment on logout, either switch the identity you send in
attributes
(e.g., fromid
todeviceId
) or clear the underlying storage used by your sticky service.
Prerequisites
Flags can depend on other flags via prerequisites. If a prerequisite is not met, the dependent flag will resolve to its fallback value. Ensure attributes satisfy prerequisite rules before evaluating. Read more about Prerequisite features
let enabled = gb.isOn(feature: "child-feature")
SemVer targeting
Map your app version/build into attributes and target with semver rules (e.g., rollouts to ">=1.2.0").
import Foundation
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
var attrs = gb.getAttributes()
attrs["appVersion"] = appVersion
gb.setAttributes(attrs)
v2 hashing
The SDK supports v2 hashing for cross-SDK parity and consistent bucketing. When migrating, verify exposure stability during QA across platforms.
Hashing Algorithms
The Swift SDK implements sophisticated hashing algorithms for consistent user bucketing and secure attribute handling:
FNV32a Hashing (Fowler-Noll-Vo)
The SDK uses the FNV32a algorithm for user bucketing and experiment assignment:
- Algorithm: Fowler-Noll-Vo hash function (fnv32a variant)
- Purpose: Consistent user bucketing across all SDK platforms
- Output: 32-bit unsigned integer converted to float between 0 and 1
- Cross-Platform: Ensures identical bucketing behavior across iOS, Android, Web, and server-side SDKs
Hash Version 2 (Recommended)
The SDK supports v2 hashing which fixes bias issues present in the original algorithm:
// v2 hashing formula (unbiased)
n = fnv32a(fnv32a(seed + value) + "")
return (n % 10000) / 10000
V2 Hashing is:
- Unbiased: Eliminates parallel experiment bias.
- Higher Precision: Uses 10,000 buckets instead of 1,000
- Consistent: Identical behavior across all GrowthBook SDKs
Experimentation and tracking
Set trackingCallback
to capture exposures whenever a user is assigned to a variation. Send these to your analytics provider. The callback provides experiment.key
and experimentResult.variationId
.
Generic example:
let gbTracking = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: [:],
trackingCallback: { experiment, result in
print("Viewed Experiment", experiment.key, "variation:", result.variationId)
}
).initializer()
Firebase Analytics example:
import FirebaseAnalytics
let gbFirebase = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: [:],
trackingCallback: { experiment, result in
Analytics.logEvent("gb_exposure", parameters: [
"experiment_key": experiment.key,
"variation_id": result.variationId
])
}
).initializer()
Experiment Result subscriptions
Subscribe to the experiments to receive callbacks for every experiment run.
// Subscribe to experiment results
gb.subscribe { experiment, result in
print("Experiment: \(experiment.key), Variation: \(result.variationId)")
}
// Clear all subscriptions when done
gb.clearSubscriptions()
Security Features
The Swift SDK implements comprehensive security measures to protect sensitive data and ensure secure feature flag evaluation:
Secure Attributes
When secure attribute hashing is enabled in your SDK Connection, you must manually hash sensitive attributes before passing them to the SDK. This allows you to safely target users based on sensitive data without exposing it to the client.
- Manual Hashing Required: You must hash
secureString
orsecureString[]
attributes using SHA-256 - Salt Protection: Use your organization's secure attribute salt for additional security
- Privacy Compliance: Enables targeting based on sensitive data without exposing it to the client
- GDPR/CCPA Ready: Helps maintain compliance with data protection regulations
- Format:
SHA256(attribute_value + organization_salt)
- Implementation: Use Apple's CryptoKit framework for SHA-256 hashing
import CryptoKit
// Helper function to hash secure attributes
func hashSecureAttribute(_ value: String, salt: String) -> String {
let data = (value + salt).data(using: .utf8)!
let hash = SHA256.hash(data: data)
return hash.compactMap { String(format: "%02x", $0) }.joined()
}
// Your organization's secure attribute salt (set in Organization Settings)
let salt = "your_organization_salt"
// Hash sensitive attributes before passing to SDK
let email = "user@example.com"
let phone = "+1234567890"
gb.setAttributes([
"id": "user_123",
"email": hashSecureAttribute(email, salt: salt), // Must be manually hashed
"phone": hashSecureAttribute(phone, salt: salt), // Must be manually hashed
"country": "US" // Regular attribute, not hashed
])
Important: When secure attribute hashing is enabled, all targeting conditions in the SDK payload referencing attributes with datatype secureString
or secureString[]
will be anonymized via SHA-256 hashing. You must manually hash these attributes using your organization's secure attribute salt.
Remote Evaluation Security
For maximum security, use Remote Evaluation mode:
- Server-Side Processing: Feature evaluation happens on your secure servers
- No Client Exposure: Targeting rules and unused variations never reach the client
- Minimal Payload: Only evaluated results are transmitted
- Enhanced Privacy: Sensitive business logic remains server-side
// Remote evaluation for maximum security
let gbSecure = GrowthBookBuilder(
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: ["id": "user_123"],
trackingCallback: { _, _ in },
remoteEval: true // Enable remote evaluation
).initializer()
Troubleshooting and debugging
- Comprehensive logging
- Set your app/logger to debug or trace during integration to observe evaluation decisions, cache reads/writes, and SSE state changes.
- Log levels:
.debug
,.info
,.warning
,.error
are supported and usesetLogLevel()
to configure the level.
- Log levels:
- Built-in OS logging integration (uses
os.Logger
on iOS 14+ andOSLog
on older versions) - QA Mode (
setQAMode()
) functionality and use cases - Be mindful to disable verbose logs in production.
- Set your app/logger to debug or trace during integration to observe evaluation decisions, cache reads/writes, and SSE state changes.
- Network errors
- The SDK retries transient failures with backoff when fetching or streaming. Inspect your networking layer logs and verify headers/hosts.
- Confirm that custom headers are accepted by your proxy/CDN and not stripped.
- SSE connection status monitoring
- Use your own telemetry around
refreshHandler
or UI updates to infer connectivity. Consider persisting the last event id to resume streams as shown above.
- Use your own telemetry around
// Example: simple log wrapper
func log(_ message: String) {
#if DEBUG
print("[GrowthBook] \(message)")
#endif
}
Reference
View detailed docs on the GitHub Repo
Supported Features
FeaturesAll versions
ExperimentationAll versions
Remote Evaluation≥ v1.0.50
Sticky Bucketing≥ v1.0.49
Prerequisites≥ v1.0.49
v2 Hashing≥ v1.0.43
Streaming≥ v1.0.43
SemVer Targeting≥ v1.0.38
Encrypted Features≥ v1.0.35