4

I have a controller that is registered as an observer for a LOT of properties on views. This is our -observeValueForKeyPath:::: method:

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void*)context
{

   if( context == kStrokeColorWellChangedContext )
   {
      [self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kStrokeColorProperty];
   }
   else if( context == kFillColorWellChangedContext )
   {
      [self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kFillColorProperty];
   }
   else if( context == kBodyStyleNumChangedContext )
   {
      [self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kBodyStyleNumProperty];
   }
   else if( context == kStyleChangedContext )
   {
      [self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kStyleProperty];
   }
   else if( context == kStepStyleChangedContext )
   {
      [self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kStepStyleProperty];
   }
   else if( context == kFirstHeadStyleChangedContext )
   {
      [self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kFirstHeadStyleProperty];
   }
   else if( context == kSecondHeadStyleChangedContext )
   {
      [self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:kSecondHeadStyleProperty];
   }

And there's actually about 3x more of these else if statements.
One thing you can see is that each block has the same code, which makes me think that it's possible to optimize this.

My initial thought was to have an NSDictionary called keyPathForContextDictionary where the keys are the constants with the Context suffix (of type void*), and the values are the appropriate string constants, denoted by the Property suffix

Then this method would only need one line:

[self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:keyPathForContextDictionary[context]];

Note that I need to use a data structure of some sort to identify which keyPath to use, and I can't simply use the keyPath argument passed into the method. This is because there are multiple views that have the same property I'm observing (for example, color wells have the color property). So each view needs to determine a unique keypath, which is currently being determined based off of the context

The problem with this is that you cannot use void* as keys in an NSDictionary. So... does anybody have any recommendations for what I could do here?

EDIT: Here's an example of how the constants are defined:

void * const kStrokeColorWellChangedContext = (void*)&kStrokeColorWellChangedContext;
void * const kFillColorWellChangedContext = (void*)&kFillColorWellChangedContext;
void * const kBodyStyleNumChangedContext = (void*)&kBodyStyleNumChangedContext;
void * const kStyleChangedContext = (void*)&kStyleChangedContext;

NSString *const kStrokeColorProperty     = @"strokeColor";
NSString *const kFillColorProperty       = @"fillColor";
NSString *const kShadowProperty          = @"shadow";
NSString *const kBodyStyleNumProperty    = @"bodyStyleNum";
NSString *const kStyleProperty           = @"style";
A O
  • 5,516
  • 3
  • 33
  • 68
  • The type of the argument `context` is `void *` so that you can use any type you want. You can change it to be an object -- you just have to cast it. What are the types of all the `k$THINGYColorWellChangedContext` constants? – jscs Oct 14 '15 at 21:06
  • Oh Sorry, let me edit the post. But the `context` suffix denotes a `void*` and a `property` suffix denotes an `NSString*` – A O Oct 14 '15 at 21:10
  • Show the declaration of one of those constants, please. – jscs Oct 14 '15 at 21:14
  • Absolutely, just added them into the original question – A O Oct 14 '15 at 21:16

2 Answers2

1

The type void * is not so much a type unto itself that you have to match, as it is "generic pointer". It's used for the context argument precisely so that you can use any underlying type that you like, including an object type. All you have to do is perform the proper casts.

You can therefore change your kTHINGYChangedContexts to be NSStrings or any other object you like very easily, and then use them as keys in your context->key path mapping.

Start with:

NSString * const kStrokeColorWellChangedContext = @"StrokeColorWellChangedContext";

When you register for observation, you must perform a bridged cast:

[colorWell addObserver:self
            forKeyPath:keyPath
               options:options
               context:(__bridge void *)kStrokeColorWellChangedContext];

Then when the observation occurs, you do the reverse cast:

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void*)ctx
{
    NSString * context = (__bridge NSString *)ctx;
    // Use context, not ctx, from here on.
}

And proceed to your key path lookup from there.

jscs
  • 63,694
  • 13
  • 151
  • 195
  • Thanks for the answer! My brain is a little fried atm, and it's hard to figure out what __bridge does. Could you by-chance give your explanation of the purpose of it? If not that's cool, I can sit down and read the docs later. Either way I'm going to try your solution right now – A O Oct 14 '15 at 21:44
  • ARC won't let you just cast an object to `void *` because it can't keep track of what you do with it after that. You use `__bridge` to make explicit "Yes, ARC, I know what I'm doing, don't worry about this pointer". More info: [ARC and bridged cast](http://stackoverflow.com/q/7036350) – jscs Oct 14 '15 at 21:46
  • Gotcha. Do you know what it's doing specifically that's telling ARC to not worry? I think I have seen this used when casting objects from C++ into Objective-C objects; so I had originally thought it was only used for C++ stuff – A O Oct 14 '15 at 21:51
  • Also, your solution worked :D I did find an alternative though, and wanted to see what you thought. I found that you could actually use `[NSValue valueWIthPointer:]` and pass in my `void*` context as an argument. I can then use this `NSValue` as the key to my dictionary. This requires less code changing for me, because then I won't have to change my constants into `NSStrings`, which may cause issues elsewhere – A O Oct 14 '15 at 21:53
  • Yup, `NSValue` would a good solution as well. You should consider posting that as an answer. The `__bridge` is a compile-time feature, like any other cast. It just stops the ARC step of compilation from inserting retains or releases in the context of the cast. – jscs Oct 14 '15 at 21:57
  • I've left work already, but I'll take a second look tomorrow. Thanks for your time and explanations ^^ truly appreciate it. – A O Oct 14 '15 at 23:37
1

Josh Caswell had a great answer, but I didn't want to modify the type of our constants into NSStrings*

So a solution instead, was to cast the void* into NSValues w/ -valueWithPointer. This way I could use the void* as keys in my dictionary

Here's the code:

   NSString *toolKeyPath = [[ToolController keyPathFromContextDictionary] objectForKey:[NSValue valueWithPointer:context]];

   if( toolKeyPath )
   {
      if( [change objectForKey:NSKeyValueChangeNewKey] == (id)[NSNull null] )
      {
         [self setValue:nil forKey:toolKeyPath];
      }
      else
      {
         [self setValue:[change objectForKey:NSKeyValueChangeNewKey] forKey:toolKeyPath];
      }
   }

And the dictionary:

+(NSDictionary*) keyPathFromContextDictionary
{
   return @{
             [NSValue valueWithPointer:kStrokeColorWellChangedContext] : kStrokeColorProperty,
             [NSValue valueWithPointer:kFillColorWellChangedContext] : kFillColorProperty,
             [NSValue valueWithPointer:kBodyStyleNumChangedContext] : kBodyStyleNumProperty,
             [NSValue valueWithPointer:kStyleChangedContext] : kStyleProperty,
             [NSValue valueWithPointer:kStepStyleChangedContext] : kStepStyleProperty,
             [NSValue valueWithPointer:kFirstHeadStyleChangedContext] : kFirstHeadStyleProperty,
             [NSValue valueWithPointer:kSecondHeadStyleChangedContext] : kSecondHeadStyleProperty,
             [NSValue valueWithPointer:kShadowChangedContext] : kShadowProperty,
             [NSValue valueWithPointer:kStrokeWidthChangedContext] : kStrokeWidthProperty,
             [NSValue valueWithPointer:kBlurRadiusChangedContext] : kBlurRadiusProperty,
             [NSValue valueWithPointer:kFontSizeChangedContext] : kFontSizeProperty
         };
}
A O
  • 5,516
  • 3
  • 33
  • 68