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:
- Clears the cached feature flags
- Fetches new feature flags for the new identity
- Emits an
identityChangedevent
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()
}
}