swift-event-broadcasting is a library for creating and observing events. It is
similar in function to the events module in Node.js.
Here, an EventBroadcaster is the analogue of Node.js EventEmitter, and an
event subscriber is the analogue of an event handler.
- "Set it and forget it" event subscription
- Support for broadcasting multiple event types
- Hassle-free unsubscribe mechanism for
Hashablesubscribers - Fully customizable event queueing and dispatching
- Type-safe events with generic payload support
- Async/await integration with AsyncStream
- Combine support with Publisher APIs
- Convenience methods:
once,filter,map, and more - Debugging utilities: logging, introspection, and statistics
- Subscriber counting and introspection utilities
Extend EventBroadcaster or implement the EventBroadcasting protocol:
import Events
class GPSService: EventBroadcaster {
}
class GPSServiceAlternate: EventBroadcasting {
private let broadcaster: EventBroadcaster = EventBroadcaster()
func subscribe(...) { broadcaster.subscribe(...) }
...
func broadcast(...) { broadcaster.broadcast(...) }
}Subscribe with an event type, the event handler as a closure, and
optionally an associated AnyHashable.
Without an AnyHashable, a SubscriberId will be returned. If you intend on
unsubscribing (removing) the event handler, then you should store the
subscriber id to call unsubscribe() later.
let gpsService = GPSService()
// Subscribe
let subscriberId = gpsService.subscribe(to: "locationUpdate") { event in
print("location updated")
}
// Broadcast
gpsService.broadcast(Event(eventType: "locationUpdate"))
// prints "location updated"
// Unsubscribe
gpsService.unsubscribe(id: subscriberId, from: "locationUpdate")With an AnyHashable, no subscriber id will be returned. To unsubscribe, pass
the same AnyHashable.
let gpsService = GPSService()
let someHashable: AnyHashable = ...
// Subscribe
gpsService.subscribe(someHashable, to: "locationUpdate") { event in
print("location updated")
}
// Broadcast
gpsService.broadcast(Event(eventType: "locationUpdate"))
// prints "location updated"
// Unsubscribe
gpsService.unsubscribe(subscriber: someHashable, from: "locationUpdate")Use TypedEvent for compile-time type safety with event payloads:
// Define a custom event type
struct LocationData {
let latitude: Double
let longitude: Double
}
let eventType = "locationUpdate"
// Subscribe with type safety
gpsService.subscribe(to: eventType) { (event: TypedEvent<LocationData>) in
print("Location: \(event.payload.latitude), \(event.payload.longitude)")
}
// Broadcast with typed data
let location = LocationData(latitude: 37.7749, longitude: -122.4194)
gpsService.broadcast(TypedEvent(eventType: eventType, payload: location))Stream events using modern Swift concurrency:
// Stream all events of a type
Task {
for await event in gpsService.events(for: "locationUpdate") {
print("Received event: \(event)")
}
}
// Wait for a single event
Task {
let event = await gpsService.nextEvent(for: "locationUpdate")
print("Got event: \(event)")
}Use Combine publishers for reactive programming:
import Combine
// Create a publisher for events
let cancellable = gpsService.publisher(for: "locationUpdate")
.sink { event in
print("Received: \(event)")
}
// Type-safe publisher
let typedCancellable = gpsService.typedPublisher(for: "locationUpdate")
.map { (event: TypedEvent<LocationData>) in event.payload }
.sink { location in
print("Location: \(location.latitude), \(location.longitude)")
}// Subscribe to receive only the first event
gpsService.once(to: "locationUpdate") { event in
print("First location update received")
}// Only receive events that match a condition
gpsService.subscribe(
to: "locationUpdate",
filter: { event in
guard let typed = event as? TypedEvent<LocationData> else { return false }
return typed.payload.latitude > 0
}
) { event in
print("Northern hemisphere location: \(event)")
}// Transform events before handling
gpsService.subscribe(
to: "locationUpdate",
map: { event -> String? in
guard let typed = event as? TypedEvent<LocationData> else { return nil }
return "\(typed.payload.latitude),\(typed.payload.longitude)"
}
) { coordinates in
print("Coordinates: \(coordinates)")
}// Check subscriber count
let count = gpsService.subscriberCount(for: "locationUpdate")
print("Subscribers: \(count)")
// Get all event types with subscribers
let types = gpsService.subscribedEventTypes()
print("Active event types: \(types)")
// Remove all subscribers for an event type
gpsService.unsubscribeAll(from: "locationUpdate")
// Remove all subscribers for all events
gpsService.unsubscribeAll()Customize how events are dispatched by providing your own EventDispatching implementation:
class CustomDispatcher: EventDispatching {
func dispatch(_ event: Event, using eventHandler: @escaping EventHandler) {
// Custom dispatching logic
DispatchQueue.main.async {
eventHandler(event)
}
}
}
let service = GPSService(eventDispatcher: CustomDispatcher())Enable debug logging to see what's happening with your events:
let service = GPSService()
// Enable debug logging with default options (logs everything)
service.enableDebugLogging()
// Or customize what gets logged
service.enableDebugLogging(options: [.logBroadcasts, .logSubscriptions])
// Custom logger
service.enableDebugLogging { message in
os_log("%{public}@", log: .default, type: .debug, message)
}Get detailed information about broadcaster state:
// Print debug info to console
service.printDebugInfo()
// Get debug description as string
let description = service.debugDescription()
// Get statistics dictionary
let stats = service.statistics()
print("Total subscribers: \(stats["totalSubscribers"]!)")
print("Event types: \(stats["eventTypes"]!)")Wrap any dispatcher with logging:
let baseDispatcher = DispatchQueueEventDispatcher.EventDispatcher()
let loggingDispatcher = LoggingEventDispatcher(wrapping: baseDispatcher) { message in
print("Event: \(message)")
}
let service = GPSService(eventDispatcher: loggingDispatcher)