Skip to main content

Advanced Usage

Advanced features and patterns for the Toggly iOS SDK.

Entity context​

setIdentity is user context only. Entity kinds use local registerContext and per-evaluation entity. See Entity context and Entity & page context. iOS never PUTs schemas.

Identity Management​

Setting User Identity​

Associate feature flag evaluations with a specific user:

import TogglyCore

// Set identity at initialization
let config = TogglyConfig(
appKey: "your-app-key",
identity: "user-123"
)

// Or set identity later
Task {
await Toggly.shared.setIdentity("user-456")
}

Identity Changes​

When identity changes, the SDK automatically:

  1. Clears the cached feature flags
  2. Fetches new feature flags for the new identity
  3. Emits an identityChanged event
Task {
// User logs in
let response = await Toggly.shared.setIdentity("user-123")
print("Flags for user: \(response.flags)")

// User logs out - revert to device ID
await Toggly.shared.setIdentity(nil)
}

Device ID Fallback​

When no identity is set, the SDK generates and persists a unique device ID:

Task {
await Toggly.shared.initialize()
let debugInfo = await Toggly.shared.getDebugInfo()
print("Device/User ID: \(debugInfo.identity ?? "none")")
}

Event Handling​

Subscribe to All Events​

Task {
let unsubscribe = await Toggly.shared.on { event in
switch event {
case .initialized(let response):
print("Initialized: \(response.status)")

case .refreshed(let flags):
print("Refreshed with \(flags.count) flags")

case .error(let errorEvent):
print("Error: \(errorEvent.error)")

case .identityChanged(let change):
print("Identity changed: \(change.previousIdentity ?? "nil") → \(change.newIdentity)")

case .featureChanged(let change):
print("Feature '\(change.featureKey)' changed: \(change.previousValue ?? false) → \(change.newValue ?? false)")

case .networkChanged(let state):
print("Network: \(state.isConnected ? "online" : "offline")")

case .appStateChanged(let state):
print("App state: \(state.rawValue)")
}
}

// Later: unsubscribe
unsubscribe()
}

Feature State Change Handlers​

Subscribe to specific feature flag changes:

Task {
let unsubscribe = await Toggly.shared.addStateChangeHandler { featureKey, previousValue, newValue in
print("\(featureKey): \(previousValue ?? false) → \(newValue ?? false)")

// React to specific changes
if featureKey == "dark-mode" {
NotificationCenter.default.post(
name: .darkModeChanged,
object: newValue
)
}
}

// Later: unsubscribe
unsubscribe()
}

Feature Gate Patterns​

Multiple Feature Evaluation​

Task {
// All features must be enabled
let allEnabled = await Toggly.shared.evaluateFeatureGate(
featureKeys: ["feature-a", "feature-b", "feature-c"],
requirement: .all
)

// Any feature must be enabled
let anyEnabled = await Toggly.shared.evaluateFeatureGate(
featureKeys: ["promo-a", "promo-b", "promo-c"],
requirement: .any
)

// Negated: true when all features are OFF
let allDisabled = await Toggly.shared.evaluateFeatureGate(
featureKeys: ["maintenance-mode"],
requirement: .all,
negate: true
)
}

Complex Feature Logic​

Task {
let isPremiumUser = await Toggly.shared.isFeatureOn("premium-tier")
let hasNewUI = await Toggly.shared.isFeatureOn("new-ui")
let isInExperiment = await Toggly.shared.isFeatureOn("experiment-group-a")

let showPremiumFeature = isPremiumUser && (hasNewUI || isInExperiment)

if showPremiumFeature {
// Show premium feature
}
}

Caching Strategies​

Aggressive Caching​

For apps that need instant feature flag access:

let config = TogglyConfig(
appKey: "your-app-key",
storage: UserDefaultsStorage(),
featureDefaults: [
"critical-feature": true // Always available
],
refreshInterval: 300 // Refresh every 5 minutes
)

Fresh Data​

For apps that need the latest flags:

let config = TogglyConfig(
appKey: "your-app-key",
refreshInterval: 30 // Refresh every 30 seconds
)

Manual Control​

Disable automatic refresh and control manually:

let config = TogglyConfig(
appKey: "your-app-key",
refreshInterval: 0 // Disable auto-refresh
)

// Refresh on specific events
func onPullToRefresh() {
Task {
await Toggly.shared.refresh()
}
}

func onAppBecameActive() {
Task {
await Toggly.shared.refresh()
}
}

Clear Cache​

Force fetch fresh data by clearing the cache:

Task {
await Toggly.shared.clearCache()
await Toggly.shared.refresh()
}

Network and App State​

Network State Updates​

Inform the SDK about network connectivity:

import Network

class NetworkMonitor {
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")

init() {
monitor.pathUpdateHandler = { path in
let state = NetworkState(
isConnected: path.status == .satisfied,
connectionType: self.connectionType(for: path)
)
Task {
await Toggly.shared.setNetworkState(state)
}
}
monitor.start(queue: queue)
}

private func connectionType(for path: NWPath) -> String? {
if path.usesInterfaceType(.wifi) { return "wifi" }
if path.usesInterfaceType(.cellular) { return "cellular" }
return nil
}
}

App State Updates​

Keep the SDK informed about app state:

// SwiftUI
.onChange(of: scenePhase) { _, phase in
Task {
switch phase {
case .active: await Toggly.shared.setAppState(.active)
case .inactive: await Toggly.shared.setAppState(.inactive)
case .background: await Toggly.shared.setAppState(.background)
@unknown default: break
}
}
}

// UIKit
NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { _ in
Task { await Toggly.shared.setAppState(.active) }
}

Testing​

Unit Testing​

import XCTest
import TogglyCore

class FeatureTests: XCTestCase {
var service: TogglyService!

override func setUp() async throws {
// Create a test service with known feature states
let config = TogglyConfig(
featureDefaults: [
"feature-a": true,
"feature-b": false
]
)
service = TogglyService(config: config)
await service.initialize()
}

func testFeatureEnabled() async {
let isEnabled = await service.isFeatureOn("feature-a")
XCTAssertTrue(isEnabled)
}

func testFeatureDisabled() async {
let isEnabled = await service.isFeatureOn("feature-b")
XCTAssertFalse(isEnabled)
}

func testFeatureGate() async {
let result = await service.evaluateFeatureGate(
featureKeys: ["feature-a", "feature-b"],
requirement: .any
)
XCTAssertTrue(result) // At least one is enabled
}
}

SwiftUI Preview Testing​

import SwiftUI
import TogglyCore
import TogglySwiftUI

struct MyView_Previews: PreviewProvider {
static var previews: some View {
// Preview with feature enabled
let enabledService = TogglyService(config: TogglyConfig(
featureDefaults: ["new-feature": true]
))

// Preview with feature disabled
let disabledService = TogglyService(config: TogglyConfig(
featureDefaults: ["new-feature": false]
))

Group {
MyView()
.togglyService(enabledService)
.previewDisplayName("Feature Enabled")

MyView()
.togglyService(disabledService)
.previewDisplayName("Feature Disabled")
}
}
}

Resetting for Tests​

override func tearDown() {
Toggly.reset()
}

Error Handling​

Graceful Degradation​

Task {
let response = await Toggly.shared.initialize()

switch response.status {
case .fetched:
print("Loaded \(response.flags.count) flags from server")
case .cached:
print("Using cached flags")
case .defaults:
if let error = response.error {
print("Using defaults due to error: \(error)")
} else {
print("Using configured defaults")
}
}
}

Error Events​

Task {
_ = await Toggly.shared.on { event in
if case .error(let errorEvent) = event {
// Log error
print("Toggly error: \(errorEvent.error)")

// Report to crash reporting service
Crashlytics.log("Toggly error: \(errorEvent.error)")
}
}
}

Performance Optimization​

Minimize Evaluations​

// Bad: Multiple evaluations in render
var body: some View {
VStack {
if await isFeatureOn("feature") { View1() }
if await isFeatureOn("feature") { View2() }
if await isFeatureOn("feature") { View3() }
}
}

// Good: Single evaluation with property wrapper
struct MyView: View {
@FeatureFlag("feature") var isFeatureEnabled

var body: some View {
VStack {
if isFeatureEnabled { View1() }
if isFeatureEnabled { View2() }
if isFeatureEnabled { View3() }
}
}
}

Batch Evaluations​

// Evaluate all needed flags at once
Task {
let features = await Toggly.shared.currentFeatures ?? [:]
let showDashboard = features["new-dashboard"] ?? false
let showBanner = features["promo-banner"] ?? false
let isPremium = features["premium-tier"] ?? false

await MainActor.run {
updateUI(dashboard: showDashboard, banner: showBanner, premium: isPremium)
}
}

Cleanup​

Proper Disposal​

class MyCoordinator {
private var togglyService: TogglyService?

func start() {
togglyService = TogglyService(config: config)
Task {
await togglyService?.initialize()
}
}

func stop() {
Task {
await togglyService?.dispose()
togglyService = nil
}
}
}

App Termination​

func applicationWillTerminate(_ application: UIApplication) {
Task {
await Toggly.shared.dispose()
}
}