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)
}
}