UIKit Support
The TogglyUIKit package provides UIKit-specific utilities and extensions for feature flag management.
Telemetry and session ownership
Use the existing Core service for frontend telemetry
and session token rotation. Telemetry is on by
default with an app key; configure enableTelemetry: false to opt out. metricsBaseUrl and
telemetryFlushIntervalMs are independent of definition refresh settings.
UI evaluation records checks; record usage and views explicitly from the real
interaction or navigation callback. Forward background lifecycle to the owning
service and dispose directly owned services at teardown. Storage and UI
companions reuse the owner; they do not create additional reporters.
Installation
Add TogglyUIKit to your target:
.product(name: "TogglyUIKit", package: "Toggly.FeatureManagement")
FeatureFlagViewController
A base view controller that provides feature flag observation and automatic UI updates.
Basic Usage
import TogglyUIKit
class MyViewController: FeatureFlagViewController {
@IBOutlet weak var newFeatureView: UIView!
@IBOutlet weak var legacyFeatureView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Start observing features
observeFeature("new-dashboard")
observeFeature("beta-mode")
}
override func featureFlagDidChange(_ key: String, isEnabled: Bool) {
switch key {
case "new-dashboard":
newFeatureView.isHidden = !isEnabled
legacyFeatureView.isHidden = isEnabled
case "beta-mode":
configureBetaMode(isEnabled)
default:
break
}
}
private func configureBetaMode(_ enabled: Bool) {
// Update UI for beta mode
}
}
Check Current State
class MyViewController: FeatureFlagViewController {
func updateUI() {
// Check current feature state
if isFeatureEnabled("premium-tier") {
showPremiumContent()
}
}
}
Stop Observing
class MyViewController: FeatureFlagViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopObservingFeature("temporary-feature")
}
}
Custom Service
class MyViewController: FeatureFlagViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Use a custom Toggly service instance
togglyService = customTogglyService
observeFeature("my-feature")
}
}
UIView Extensions
Visibility Binding
Bind a view's visibility to a feature flag:
import TogglyUIKit
class MyViewController: UIViewController {
@IBOutlet weak var newBannerView: UIView!
@IBOutlet weak var legacyBannerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Show when feature is enabled
newBannerView.bindToFeatureFlag("new-banner")
// Show when feature is disabled
legacyBannerView.bindToFeatureFlag("new-banner", hideWhenEnabled: true)
}
}
Unbind
Remove the feature flag binding:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
newBannerView.unbindFromFeatureFlag()
}
UIControl Extensions
Enabled State Binding
Bind a control's enabled state to a feature flag:
import TogglyUIKit
class MyViewController: UIViewController {
@IBOutlet weak var premiumButton: UIButton!
@IBOutlet weak var legacyButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Enable when feature is on
premiumButton.bindEnabledToFeatureFlag("premium-features")
// Disable when feature is on (enable when off)
legacyButton.bindEnabledToFeatureFlag("new-mode", disableWhenEnabled: true)
}
}
Async/Await Utilities
The FeatureFlagAsync enum provides async/await helpers for checking feature flags.
Basic Checks
import TogglyUIKit
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task {
// Check if feature is enabled
let isEnabled = await FeatureFlagAsync.isEnabled("my-feature")
// Check if feature is disabled
let isDisabled = await FeatureFlagAsync.isDisabled("my-feature")
await MainActor.run {
updateUI(featureEnabled: isEnabled)
}
}
}
}
Feature Gate Evaluation
Task {
// All features must be enabled
let allEnabled = await FeatureFlagAsync.evaluate(
["feature-a", "feature-b"],
requirement: .all
)
// Any feature must be enabled
let anyEnabled = await FeatureFlagAsync.evaluate(
["promo-a", "promo-b"],
requirement: .any
)
// Negated result
let notInMaintenance = await FeatureFlagAsync.evaluate(
["maintenance-mode"],
negate: true
)
}
Conditional Execution
Task {
// Execute only if feature is enabled
await FeatureFlagAsync.ifEnabled("analytics") {
trackScreenView()
}
// Execute only if feature is disabled
await FeatureFlagAsync.ifDisabled("new-api") {
useLegacyAPI()
}
// Choose between two actions
let result = await FeatureFlagAsync.choose(
"new-checkout",
enabled: { await processNewCheckout() },
disabled: { await processLegacyCheckout() }
)
}
Custom Service
All async methods accept an optional service parameter:
let customService = TogglyService(config: TogglyConfig(...))
Task {
let isEnabled = await FeatureFlagAsync.isEnabled(
"my-feature",
service: customService
)
}
Programmatic Usage
Direct Service Access
import TogglyCore
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task {
// Check feature flags
let isEnabled = await Toggly.shared.isFeatureOn("my-feature")
// Evaluate multiple features
let allEnabled = await Toggly.shared.evaluateFeatureGate(
featureKeys: ["feature-a", "feature-b"],
requirement: .all
)
await MainActor.run {
configureUI(featureEnabled: isEnabled, allFeaturesEnabled: allEnabled)
}
}
}
}
Listen for Changes
class MyViewController: UIViewController {
private var unsubscribe: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
Task {
unsubscribe = await Toggly.shared.addStateChangeHandler { [weak self] key, previous, new in
guard key == "my-feature" else { return }
Task { @MainActor in
self?.handleFeatureChange(isEnabled: new ?? false)
}
}
}
}
deinit {
unsubscribe?()
}
}
Entity context
Pass the entity on each UIKit check — not on setIdentity. See Entity & page context. Clients do not register schemas.
let on = await Toggly.shared.isFeatureOn("OrderBadge", entity: order, kind: "Order")
Gates fail closed without context.
Best Practices
1. Use FeatureFlagViewController
Subclass FeatureFlagViewController for cleaner code:
// Good: Use the base class
class MyVC: FeatureFlagViewController {
override func featureFlagDidChange(_ key: String, isEnabled: Bool) {
// Handle changes
}
}
// Avoid: Manual subscription management
class MyVC: UIViewController {
var unsubscribes: [() -> Void] = []
// More boilerplate...
}
2. Handle Main Thread
UI updates must be on the main thread:
Task {
let isEnabled = await FeatureFlagAsync.isEnabled("feature")
await MainActor.run {
self.updateUI(enabled: isEnabled)
}
}
3. Clean Up Bindings
Remove bindings when views are removed:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
myView.unbindFromFeatureFlag()
}
4. Use Feature Constants
Define feature keys as constants:
enum Features {
static let newDashboard = "new-dashboard"
static let premiumMode = "premium-mode"
}
observeFeature(Features.newDashboard)