Publish-Subscribe in Swift
For one of my current projects, I had to implement an event-based messaging system. The first implementation boiled down to a bunch of NSNotificiation
and addObserver
. But this quickly became messy and the de-coupling was that strong, that debugging would be much less fun (for more on this topic, see also Why NSNotificationCenter is Bad, which I did not believe in the very beginning, but now there is some truth in it).
So, here we go and the first thing to implement was a message broker which acts as centralized system for subscriptions and sending messages to the subscribers. I designed this as a singleton. Yes, they are considered being bad, but in this case it makes perfect sense IMO:
private let _messageBroker = MessageBroker()
typealias MessageKey = String
// Protocols
protocol Message { func messageKey() -> MessageKey }
protocol Subscriber { func receive(#message: Message) }
// Broker Implementation
class MessageBroker : NSObject
{
class var sharedMessageBroker : MessageBroker
{
return _messageBroker
}
private var subscribers = Dictionary<MessageKey, Array<Subscriber>>()
func subscribe(subscriber: Subscriber, messageKey: MessageKey)
{
if subscribers[messageKey] == nil
{
subscribers[messageKey] = []
}
subscribers[messageKey]!.append(subscriber)
}
func publish(#message: Message)
{
if let subscribers = subscribers[message.messageKey()]
{
for subscriber in subscribers
{
subscriber.receive(message: message)
}
}
}
}
Probably the most notable thing here is typealias MessageKey = String
(surprise!). Everything else is straightforward.
So, why MessageKey
? This is, because the pub-sub shall be able to use an enum
in order to apply a switch
on the received message. But, while the enum
can conform to a protocol, you cannot add it to the Dictionary
, which holds the receivers wrt a message type. Furthermore, it is currently not possible in Swift to define a protocol as Hashable
for using it as key in a dictionary, e.g. Usage of protocols as array types and function parameters in swift. Yes, one could use a class and make this one hashable, but I feel this is getting nasty.
Q: How does a bunch of messages look?
enum SomeMessages : Message
{
case Foo, case Bar
static let FooType = "Foo"
static let BarType = "Bar"
func messageKey() -> MessageKey
{
switch self
{
case .Foo : return SomeMessage.FooType
case .Bar : return SomeMessage.BarType
}
}
}
Q: How do I send a message?
MessageBroker.sharedMessageBroker.publish(message: SomeMessage.Foo)
Q: How do I subscribe?
class A : Receiver
{
init()
{
MessageBroker.sharedMessageBroker.subscribe(self, messageKey: SomeMessage.FooType)
}
func receive(#message: Message)
{
if let message = message as? SomeMessage
{
switch message
{
case .Foo : println("It is a Foo!")
}
}
}
}
Q: Why are you telling this story?
Because, I was looking around for something which also provides me the ability to trigger a callback (i.e. call it synchronous message), and this is really painful when relying on NSNotification
. With the code above you can define e.g. the following message type:
enum SyncMessage
{
case Yay(callback: () -> Void)
}
In the publisher:
let message = SyncMessage.Yay({ println("Callback, baby!") })
MessageBroker.sharedMessageBroker.publish(message: message)
And in the receiver:
switch message
{
case .Yay(let callback) : println("Yay, callbacks!"); callback()
}
Boy, how I love Swift!