Is subclassing NSNotification the right route if I want to add typed properties?
Asked Answered
Q

4

10

I am trying to subclass NSNotification.

Apple's docs for NSNotificationstate the following:

NSNotification is a class cluster with no instance variables. As such, you must subclass NSNotification and override the primitive methods name, object, and userInfo. You can choose any designated initializer you like, but be sure that your initializer does not call NSNotification’s implementation of init (via [super init]). NSNotification is not meant to be instantiated directly, and its init method raises an exception.

But this isn't clear to me. Should I create an initializer like this?

-(id)initWithObject:(id)object
{
    return self;
}
Quartis answered 27/9, 2011 at 15:53 Comment(5)
It is highly unusual to create a subclass of NSNotification. Can you explain why you feel the need to do this?Daisy
I want to use NSNotifications with strongly typed properties rather than using the generic userInfo object. I also like the fact that a subclassed notification better encapsulates its purpose. I would prefer [UserPrefsChangedNotification alloc] init] to passing a name string to an NSNotificationCenter.Quartis
you could use an NSNotification category to add some properties, and then have the method implementations attach stuff to the NSNotification using associated objects. The category could even have a convenience constructor for you.Daisy
I'm still wrapping my brain around Objective C and my instinct is to subclass, but that does make a lot of sense. Thanks for your help.Quartis
@DaveDeLong Am I missing something? It seems it is not possible to add properties in categories.Quartis
D
14

Subclassing NSNotification is an atypical operation. I think I've only seen it done once or twice in the past few years.

If you're looking to pass things along with the notification, that's what the userInfo property is for. If you don't like accessing things through the userInfo directly, you could use a category to simplify access:

@interface NSNotification (EasyAccess)

@property (nonatomic, readonly) NSString *foo;
@property (nonatomic, readonly) NSNumber *bar;

@end

@implementation NSNotification (EasyAccess)

- (NSString *)foo {
  return [[self userInfo] objectForKey:@"foo"];
}

- (NSNumber *)bar {
  return [[self userInfo] objectForKey:@"bar"];
}

@end

You can also use this approach to simplify NSNotification creation. For example, your category could also include:

+ (id)myNotificationWithFoo:(NSString *)foo bar:(NSString *)bar object:(id)object {
  NSDictionary *d = [NSDictionary dictionaryWithObjectsForKeys:foo, @"foo", bar, @"bar", nil];
  return [self notificationWithName:@"MyNotification" object:object userInfo:d];
}

If, for some strange reason, you'd need the properties to be mutable, then you'd need to use associative references to accomplish that:

#import <objc/runtime.h>
static const char FooKey;
static const char BarKey;

...

- (NSString *)foo {
  return (NSString *)objc_getAssociatedObject(self, &FooKey);
}

- (void)setFoo:(NSString *)foo {
  objc_setAssociatedObject(self, &FooKey, foo, OBJC_ASSOCIATION_RETAIN);
}

- (NSNumber *)bar {
  return (NSNumber *)objc_getAssociatedObject(self, &BarKey);
}

- (void)setBar:(NSNumber *)bar {
  objc_setAssociatedObject(self, &BarKey, bar, OBJC_ASSOCIATION_RETAIN);
}

...
Daisy answered 28/9, 2011 at 16:28 Comment(4)
Great explaination. Thanks a lotQuartis
On further reflection this doesn't really solve the problem I have. Adding categories is fine if I want all instances of NSNotification to gain this extra functionality, but I want the option to have different specialised notifications. Perhaps one will be carrying a payload of type NSArray, whilst another might be carrying a Some Class as its payload. I understand that I can use userInfo as a kind of generic dump for the data a notification carries, but I think its much cleaner to have a notification specialise.Quartis
This is a great solution for this problem. You can make a different category for each "special" notification you want.Sunlight
Good explanation of using a category; this is a good solution in most cases. But, I don't understand the implied aversion to sub-classing NSNotification. It doesn't strike me as being a bad practice - Apple even discuss doing it - and should yield better performance than looking up all 'fields' from the userInfo dictionary, in performance critical code. I'm using an NSNotification subclass for this reason in my current project. Dave DeLong, did you have any other specific reasons not to take this approach?Arondell
Q
2

It seems this does work. For example:

#import "TestNotification.h"

NSString *const TEST_NOTIFICATION_NAME = @"TestNotification";

@implementation TestNotification

-(id)initWithObject:(id)object
{
    object_ = object;
    return self;
}

-(NSString *)name
{
    return TEST_NOTIFICATION_NAME;
}

-(id)object
{
    return object_;
}

- (NSDictionary *)userInfo
{
    return nil;
}

@end

also beware a massive Gotcha related to NSNotifications. The type of NSNotifications greated using NSNotification notificationWithName:object: is NSConcreteNotification, not NSNotification. And to make it a little more awkward, if you are checking for class, NSConcreteNotification is private so you have nothing to compare to.

Quartis answered 27/9, 2011 at 16:27 Comment(1)
With class clusters you should expect to get anything back that is documented and sometimes that anything is literally anything that replies YES to isKindOfRaddi
B
1

You don’t set it, exactly—you just override the implementation of the name method so it returns what you want. In other words:

- (NSString *)name
{
    return @"Something";
}

Your initializer looks fine—I haven’t seen an example of an init that doesn’t call its superclass’s implementation before, but if that’s what the doc’s saying you should do, it’s probably worth a try.

Backpack answered 27/9, 2011 at 16:0 Comment(1)
yep. I realised I should override and return the desired value about a second after I posted this, but you must have got in there before I edited it out. Thanks for replying.Quartis
F
1

You can pass a userInfo argument when delivering a notification. Why not create a payload and send that.

// New file:

@interface NotificationPayload : NSObject
@property (copy, nonatomic) NSString *thing;
@end

@implementation NotificationPayload
@end

// Somewhere posting:

NotificationPayload *obj = [NotificationPayload new];
obj.thing = @"LOL";

[[NSNotificationCenter defaultCenter] postNotificationName:@"Hi" object:whatever userInfo:@{ @"payload": obj }];

// In some observer:

- (void)somethingHappened:(NSNotification *)notification
{
  NotificationPayload *obj = notification.userInfo[@"payload"];
  NSLog(@"%@", obj.thing);
}

Done.

As a side note: I've found over the years that making a conscious effort to avoid subclassing has made my code more clean, maintainable, changeable, testable and extensible. If you can solve the problem using protocols or categories then you wont lock yourself into the first shoddy design you come up with. With Swift 2.0 protocol extensions in the mix we're really laughing too.

Footgear answered 15/7, 2015 at 11:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.