Combine Publishers
The TogglyCombine package provides Combine publishers for reactive feature flag subscriptions.
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 TogglyCombine to your target:
.product(name: "TogglyCombine", package: "Toggly.FeatureManagement")
Feature Flag Publishers
FeatureFlagPublisher
Subscribe to a single feature flag's state:
import Combine
import TogglyCombine
class ViewModel: ObservableObject {
@Published var isFeatureEnabled = false
private var cancellables = Set<AnyCancellable>()
init() {
FeatureFlagPublisher("new-dashboard")
.receive(on: DispatchQueue.main)
.assign(to: &$isFeatureEnabled)
}
}
With Default Value
Provide a default value for when the flag isn't loaded:
FeatureFlagPublisher("new-feature", defaultValue: true)
.sink { isEnabled in
print("Feature enabled: \(isEnabled)")
}
.store(in: &cancellables)
With Custom Service
Use a specific Toggly service instance:
let customService = TogglyService(config: customConfig)
FeatureFlagPublisher("my-feature", service: customService)
.sink { isEnabled in
// Handle state
}
.store(in: &cancellables)
Feature Gate Publishers
FeatureGatePublisher
Evaluate multiple feature flags with AND/OR logic:
import TogglyCombine
class ViewModel: ObservableObject {
@Published var hasPremiumFeatures = false
@Published var hasAnyPromotion = false
private var cancellables = Set<AnyCancellable>()
init() {
// All features must be enabled
FeatureGatePublisher(
["premium-tier", "new-features"],
requirement: .all
)
.receive(on: DispatchQueue.main)
.assign(to: &$hasPremiumFeatures)
// Any feature must be enabled
FeatureGatePublisher(
["promo-a", "promo-b", "promo-c"],
requirement: .any
)
.receive(on: DispatchQueue.main)
.assign(to: &$hasAnyPromotion)
}
}
With Negation
Invert the gate result:
// True when maintenance mode is OFF
FeatureGatePublisher(
["maintenance-mode"],
negate: true
)
.sink { isAvailable in
// isAvailable is true when maintenance-mode is OFF
}
.store(in: &cancellables)
Event Publishers
TogglyEventPublisher
Subscribe to all Toggly SDK events:
import TogglyCombine
class EventLogger {
private var cancellables = Set<AnyCancellable>()
init() {
TogglyEventPublisher()
.sink { event in
switch event {
case .initialized(let response):
print("Initialized with \(response.flags.count) flags")
case .refreshed(let flags):
print("Refreshed: \(flags)")
case .error(let errorEvent):
print("Error: \(errorEvent.error)")
case .identityChanged(let change):
print("Identity: \(change.previousIdentity ?? "nil") → \(change.newIdentity)")
case .featureChanged(let change):
print("\(change.featureKey): \(change.previousValue ?? false) → \(change.newValue ?? false)")
case .networkChanged(let state):
print("Network: \(state.isConnected ? "online" : "offline")")
case .appStateChanged(let state):
print("App state: \(state)")
}
}
.store(in: &cancellables)
}
}
FeatureChangedPublisher
Subscribe specifically to feature flag changes:
import TogglyCombine
class FeatureChangeTracker {
private var cancellables = Set<AnyCancellable>()
init() {
// All feature changes
FeatureChangedPublisher()
.sink { change in
print("\(change.featureKey) changed")
}
.store(in: &cancellables)
// Specific feature changes
FeatureChangedPublisher(featureKey: "important-feature")
.sink { change in
trackAnalytics(feature: change.featureKey, enabled: change.newValue ?? false)
}
.store(in: &cancellables)
}
}
Convenience Publishers
TogglyPublishers
Use static convenience methods for common patterns:
import TogglyCombine
class ViewModel: ObservableObject {
@Published var isEnabled = false
@Published var allFeaturesEnabled = false
private var cancellables = Set<AnyCancellable>()
init() {
// Feature flag
TogglyPublishers.featureFlag("my-feature")
.receive(on: DispatchQueue.main)
.assign(to: &$isEnabled)
// Feature gate
TogglyPublishers.featureGate(["feature-a", "feature-b"])
.receive(on: DispatchQueue.main)
.assign(to: &$allFeaturesEnabled)
// Events
TogglyPublishers.events()
.sink { event in
// Handle event
}
.store(in: &cancellables)
// Feature changes
TogglyPublishers.featureChanged(featureKey: "tracked-feature")
.sink { change in
// Handle change
}
.store(in: &cancellables)
}
}
Service Extensions
The TogglyService is extended with publisher factory methods:
import TogglyCombine
let service = TogglyService(config: config)
// Create publishers from the service
service.featureFlagPublisher("my-feature")
.sink { /* ... */ }
.store(in: &cancellables)
service.featureGatePublisher(["a", "b"], requirement: .all)
.sink { /* ... */ }
.store(in: &cancellables)
service.eventPublisher()
.sink { /* ... */ }
.store(in: &cancellables)
service.featureChangedPublisher(featureKey: "specific-feature")
.sink { /* ... */ }
.store(in: &cancellables)
Usage with SwiftUI
ObservableObject Pattern
import SwiftUI
import Combine
import TogglyCombine
class FeatureFlagsViewModel: ObservableObject {
@Published var showNewDashboard = false
@Published var isPremiumUser = false
@Published var lastError: String?
private var cancellables = Set<AnyCancellable>()
init() {
setupSubscriptions()
}
private func setupSubscriptions() {
TogglyPublishers.featureFlag("new-dashboard")
.receive(on: DispatchQueue.main)
.assign(to: &$showNewDashboard)
TogglyPublishers.featureGate(["premium-tier", "active-subscription"])
.receive(on: DispatchQueue.main)
.assign(to: &$isPremiumUser)
TogglyPublishers.events()
.compactMap { event -> String? in
if case .error(let errorEvent) = event {
return errorEvent.error
}
return nil
}
.receive(on: DispatchQueue.main)
.assign(to: &$lastError)
}
}
struct ContentView: View {
@StateObject private var viewModel = FeatureFlagsViewModel()
var body: some View {
VStack {
if viewModel.showNewDashboard {
NewDashboard()
} else {
LegacyDashboard()
}
if viewModel.isPremiumUser {
PremiumBadge()
}
if let error = viewModel.lastError {
Text("Error: \(error)")
.foregroundColor(.red)
}
}
}
}
Combining Publishers
Use Combine operators to build complex logic:
import Combine
import TogglyCombine
class ComplexViewModel: ObservableObject {
@Published var canAccessFeature = false
private var cancellables = Set<AnyCancellable>()
init() {
// Combine multiple feature flags
Publishers.CombineLatest3(
FeatureFlagPublisher("feature-enabled"),
FeatureFlagPublisher("user-eligible"),
FeatureGatePublisher(["no-maintenance"], negate: false)
)
.map { enabled, eligible, notMaintenance in
enabled && eligible && notMaintenance
}
.receive(on: DispatchQueue.main)
.assign(to: &$canAccessFeature)
}
}
Entity context
Publishers still take user identity separately. Pass entity on each publisher/eval. See Entity & page context. Clients do not register schemas.
Best Practices
1. Always Receive on Main Thread
UI updates must happen on the main thread:
FeatureFlagPublisher("feature")
.receive(on: DispatchQueue.main) // Important!
.assign(to: &$isEnabled)
2. Store Cancellables
Always store subscriptions to prevent premature cancellation:
private var cancellables = Set<AnyCancellable>()
// Good
publisher.sink { ... }.store(in: &cancellables)
// Bad - subscription is immediately cancelled
let _ = publisher.sink { ... }
3. Use assign(to:) for @Published
Use the newer assign(to:) for @Published properties:
// Modern (iOS 14+)
publisher.assign(to: &$property)
// Legacy
publisher.assign(to: \.property, on: self).store(in: &cancellables)
4. Handle Errors
TogglyPublishers never fail, but handle error events:
TogglyPublishers.events()
.sink { event in
if case .error(let errorEvent) = event {
handleError(errorEvent.error)
}
}
.store(in: &cancellables)