Skip to main content

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)