Saturday, April 21, 2018

Even Simpler Notifications: Notification Messages

The Notification Protocols implementation I showed yesterday is actually just one in a series of implementations that came out of thinking about notifications.

The basic insight, and it is a pretty trivial one, is that a notification is just a broadcast message. So when we have real messaging, which we do, notifications consist of sending a message to a dynamically defined set of receivers.

How can we send a message, for example -modelDidChange: to a set of receivers? With HOM that's also trivial:


[[receivers do] modelDidChange:aPath];

So how do we define the set of receivers? Well, the collection is defined as all the objects listening to a particular notification, in this case this notification is the message. So instead of do, we need a notify: HOM that collects all the receivers registered for that message:


[[center notifyListenersOf:@selector(modelDidChange:)] modelDidChange:aPath];

But of course this is redundant, we already have the modelDidChange: message in the argument message of the HOM. So we can remove the argument from the notify HOM:


[[center notify] modelDidChange:aPath];

A trivial notification center now is just a dictionary of arrays of receivers, with the keys of the dictionary being the message names. With that dictionary called receiversByNotificationName, the notify HOM would look as follows:
DEFINE_HOM(notify, void)
{
    NSString *key = NSStringFromSelector([invocation selector]);
    NSArray *receivers = self.receiversByNotificationName[key];
    [[invocation do] invokeWithTarget:[receivers each]];
}

Of course, you can also integrate with NSNotificationCenter by using the converted message name as the NSNotification name.

You would then register for the notification with the following code:


-(void)registerNotificationMessage:(SEL)aMessage
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:aMessage name:NSStringFromSelector(aMessage) object:nil];
}

The HOM code is a little more involved, I'll leave that as an exercise for the reader.

So, by taking messaging seriously, we can get elegant notifications in even less code than Notification Protocols. The difference is that Notification Protocols actually let you declare adoption of a notification statically in your class's interface, and have that declaration be meaningful.


@interface MyGreatView : NSView

Another aspect is that while HOM is almost certainly too much for poor old Swift to handle, it should be capable of dealing with Notification Protocols. Watch this space!

Friday, April 20, 2018

Notification Protocols

NSNotificationCenter is straightforward mechanism for implementing a clean MVC architecture, but it is also somewhat cumbersome and error-prone. The receiving object has to register itself with the notification center for the particular notification, specified by a string. It also has to implement a method for handling the notification, and the registration and the method have to match. The string used has to be coordinated with the senders of the notification, with no checking whatsoever. The compiler doesn't help with the manual bookkeeping and there is no indication in the interface that the class receives notifications.

Let's fix this with Notification Protocols.

Notification Protocols: Usage

First, let's define a protocol for our notification. We want a ModelDidChange notification. We also want common message to be sent to our objects, in this case the message -modelDidChange:.
@protocol ModelDidChange <MPWNotificationProtocol>

-(void)modelDidChange:(NSNotifiction*)notification;

@end

This protocol must contain a single message and conform to MPWNotificationProtocol, which indicates that this is a notification protocol. This way, we define that a particular notification always maps to one specific message, but this is common and should probably be considered a best-practice.

Second, a class wishing to adopt this notification must conform to the protocol:


@interface NotifiedView:NSView <ModelDidChange>

@end

NotifiedView must implement -(void)modelDidChange:(NSNotificaiton*)notification;, but fortunately the compiler will tell us if we forget to do this, because of the protocol conformance declaration. Finally, and this is the part that can't really be checked, the object must call [self installProtocolNotifications] somewhere in its initializer. It's probably best to do this in a common superclass.

Then, just use the PROTOCOL_NOTIFY macro to send the notification. The macro takes the name of the protocol as its argument. It uses the @protocol compiler directive to turn that into the protocol and then uses the protocol's name for the notification.


PROTOCOL_NOTIFY(ModelDidChange,changedUri);

That's it! Your NotifiedView will now get the -modelDidChange: message. The string is hidden behind the use of @protocol, meaning the compiler helps a bit in checking that we are sending the right notification (there are far fewer protocols than strings). The protocol also helps us keep the mapping from notification to message straight, mostly by automating it away. It also allows us to declare conformance to the notification statically, both for readers and for checking that we actually implement the method.

Notification Protocols: Implementation

The implementation is quite simple: the -installProtocolNotifications method iterates over all the protocols an object's class conforms to. For the ones that conform to MPWNotificationProtocol it registers itself with NSotificationCenter to be sent the message in the protocol.

The implementation is part of MPWFoundation.