Skip to main content

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)