Swift
This SDK is designed for iOS and macOS applications. It uses Swift concurrency (actor) and Combine for reactive updates.
Installation
Add the client to your project using Swift Package Manager:
// In your Package.swift
dependencies: [
.package(url: "https://github.com/Tggl/swift-tggl-client", from: "1.0.0"),
]Or in Xcode, go to File > Add Package Dependencies and enter the repository URL.
The package supports iOS 16+ and macOS 14+.
Quick start
Use the TgglClient with your API key:
import TgglClient
let client = TgglClient(apiKey: "XXX")
await client.setContext(context: ["userId": "abc123"])
await client.waitReady()
let flag = await client.get(
slug: "my-feature",
defaultValue: .boolean(true)
)The client uses the remote evaluation paradigm, which means that feature flag evaluations are performed on a remote server everytime the context changes.

The context is kept as state in the client, and can be updated using the setContext method:
// User just logged out
await client.setContext(context: [:])Note that setContext triggers a network request to re-evaluate all flags. The get method itself does not perform any network request, it reads from the local state.
Since TgglClient is a Swift actor, all method calls must be made with await.
Evaluating flags
The get method is used to evaluate feature flags. It takes the flag slug as the first argument and a default TgglValue as the second argument, which is returned if the flag is not found or if an error occurs during evaluation:
let flag = await client.get(
slug: "my-feature",
defaultValue: .boolean(true)
)Choose the default value carefully to ensure your application behaves correctly even if the Tggl service is unreachable for any reasons (including your user's network) or if someone from your team inadvertently removes the flag entirely.
Flag values
Flag values are represented by the TgglValue enum, which supports multiple types:
public enum TgglValue {
case string(String)
case number(Double)
case boolean(Bool)
case array([TgglValue])
case object([String: TgglValue])
case null
}The get method returns a TgglValue directly:
let flag = await client.get(
slug: "color-button",
defaultValue: .string("#00ff00")
)
switch flag {
case .string(let color):
print("Color: \(color)")
case .boolean(let enabled):
print("Enabled: \(enabled)")
default:
break
}Getting all flags
You can retrieve all currently loaded flags at once:
let flags = await client.getFlags()Waiting for the client to be ready
The client has to load the initial state once before being able to correctly evaluate flags. If you call get before the client is ready, it will return the default value for all flags.
There are multiple ways to wait for the client to be ready:
// 1. Using waitReady (async)
await client.waitReady()
// 2. Using a Combine publisher
let cancellable = await client.isReadyPublisher()
.filter { $0 }
.first()
.sink { isReady in
// Client is ready
}
// 3. Checking the property directly
if await client.getIsReady() {
// ...
}The client is ready as soon as the first network request has been completed (even if it fails), or if the state can be retrieved from the built-in UserDefaults storage first.
Polling for "live" updates
You can update your flags at anytime from the Tggl dashboard. To get these updates "live" in your application, you can enable polling after creating the client. The interval is specified in seconds:
// Start polling every 10 seconds
await client.startPolling(every: 10)Polling can be stopped at any time:
await client.stopPolling()You can manually trigger a refresh of the state at any time:
await client.refetch()On iOS, polling is automatically paused when the app goes to the background and resumed when it becomes active again.
Reporting
By default, the SDK sends flag evaluation data to Tggl to give you precious insights on how you are actually using your flags in production and to help you manage flags lifecycles better.

Reporting is batched and sent every 5 seconds by default to reduce network usage.
You can disable reporting entirely when creating the client:
let client = TgglClient(
apiKey: "XXX",
reporting: false
)Using the proxy
Having a proxy between your application and the Tggl API can help reduce latency, improve reliability, and reduce costs. You can easily configure the client to use a proxy by setting the baseUrls option:
let client = TgglClient(
apiKey: "XXX",
baseUrls: ["https://my-tggl-proxy.example.com"]
)Note that baseUrls is an array, so you can provide multiple URLs for redundancy. The client will automatically rotate through the list of URLs (in order) in case of failures. The client will always fall back to the Tggl API if it is not already present in the list. You can also provide both proxy URLs and the Tggl API URL in the same list if you want to control the order of preference.
let client = TgglClient(
apiKey: "XXX",
baseUrls: [
"https://my-tggl-proxy1.example.com",
"https://api.tggl.io",
"https://my-tggl-proxy2.example.com",
]
)The baseUrls you provide will automatically be passed down to the TgglReporting instance used by the client, so reporting will also go through the proxy.
Retry mechanism
Because network requests can fail for various reasons (network issues, server issues, etc.), the client has a built-in retry mechanism with an exponential backoff policy:
let client = TgglClient(
apiKey: "XXX",
// Retry at most 2 times after first failure (default is 3)
maxRetries: 2,
// Timeout requests after 5 seconds (default is 8 seconds)
timeoutMs: 5_000
)You can disable the retry mechanism by setting maxRetries to 0, but this is not recommended in production environments. Timeouts are applied to each individual request, not to the whole retry sequence.
The retry mechanism is applied to each baseUrls individually. For example, if you have 3 URLs and maxRetries is set to 2, the client will try each URL up to 3 times (1 initial try + 2 retries) before moving to the next URL.
Tracking
To react to flag changes, you can use Combine publishers to observe individual flags:
let cancellable = await client.publisher(for: "my-feature")
.sink { flag in
print("Flag updated:", flag.key, flag.value)
}The publisher emits a new value whenever the flag's value changes, with duplicates automatically removed.
Error handling
For resiliency, the client swallows all errors and returns the default value when an error occurs during flag evaluation. However, you can listen to error events using the Combine publisher:
let cancellable = await client.errorPublisher()
.sink { error in
print("Tggl client error:", error)
}Additionally, you can get the error from the last operation using the getError method:
let error = await client.getError()Gracefully shutting down
For unit tests or when your application is exiting, you may want to gracefully shut down the client to ensure that everything stops and the last reporting data is sent. You can do this using the close method:
await client.close()This will stop any background tasks (polling, reporting), flush remaining reporting data, and clean up resources. You should await this method to ensure that everything has been cleaned up.
Reference
TgglClient initialization
let client = TgglClient(
apiKey: String,
baseUrls: [String] = ["https://api.tggl.io"],
session: URLSession = .shared,
maxRetries: Int = 3,
timeoutMs: Int = 8_000,
appName: String? = nil,
reporting: Bool = true,
initialFetch: Bool = true
)| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey | String | required | Your Tggl API key |
baseUrls | [String] | ["https://api.tggl.io"] | Base URLs for the API, with failover support |
session | URLSession | .shared | Custom URL session for network requests |
maxRetries | Int | 3 | Maximum number of retries per URL on failure |
timeoutMs | Int | 8_000 | Timeout per request in milliseconds |
appName | String? | nil | Optional app name appended to the client identifier |
reporting | Bool | true | Enable or disable flag evaluation reporting |
initialFetch | Bool | true | Load cached flags from storage and fetch from API on init |