Skip to main content

Combine Publishers

The TogglyCombine package provides Combine publishers for reactive feature flag subscriptions.

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)