will/didChangeValueForKey: Real World Usage

  • In custom classes, I often run into a situation of wanting to expose
    a binding to an ivar which is dependent on internal variables.

    For example, I have a custom view (a "tag cloud") with many button-
    like regions, each of which can be "selected" or not.  Each has an
    index in an array, and I keep track of which are selected using an
    NSMutableIndexSet.  NSMutableIndexSet has many handy methods to
    mutate the set, easily handling multiple selection, deselection,
    "extending" the selection, etc. and it all works very nicely.

    In order to do something useful with the selection, I implemented a
    getter [1] which returns an array containing the 'text' ivars of each
    selected object.  To simplify external code, however, I would like to
    be able to make read-only bindings to this getter, but in order for
    this to work I had to go through my view class and surround every
    time I touch that NSMutableIndex set with pairs of -
    willChangeValueForKey: and -didChangeValueForKey:.

    It works fine, but there are two things that bother me about this:

    1.  I had to add -will/did pairs change in ^six^ places in my view
    class.  If I forget to add these in future revisions, bugs may be
    introduced.  Also, 12 lines of code to say something simple like
    "tell observers whenever this mutable index set changes" seems like
    alot of work for something (bindings) which is supposed to "reduce
    the code you have to write".  Is there an easier way to do this?  Or
    maybe it is more accurate to say that bindings does not really reduce
    the amount of code in the world, but it moves code from models into
    views, and since views are ^usually^ provided by others, it reduces
    the code that "you" have to write.

    2.  I have heard the warnings against calling -will/
    didChangeValueForKey without "really" changing anything in between.
    Well, consider what happens when the user clicks in an empty part of
    the view with no modifier keys:

        [self willChangeValueForKey:@"selectedTags"] ;
        [[self selectedIndexSet] removeAllIndexes] ;
        [self didChangeValueForKey:@"selectedTags"] ;

    Now, what if the user clicks there a second time?  The second -
    removeAllIndexes will actually have no effect on selectedTags.  Am I
    disobeying the rule and possibly setting myself up for a crash in
    some future OS version?

    Thanks,

    Jerry Krinock

    P.S.  My "tag cloud" view is built on the one recently posted by
    Robert Pointon [2], which is ^very^ cool.  I'll make my final version
    available after I get these concerns resolved.

    [1]...

    - (BOOL)isSelectedBoxedTag:(BoxedTag*)boxedTag {
        BOOL isSelected = NO ;

        int index = [_boxedTags indexOfObject:boxedTag] ;
        if (index != NSNotFound) {
            isSelected = [[self selectedIndexSet] containsIndex:index] ;
        }

        return isSelected ;
    }

    - (NSArray*)selectedTags {
        NSEnumerator* e = [_boxedTags objectEnumerator] ;
        NSMutableArray* selectedTags = [[NSMutableArray alloc] init] ;
        BoxedTag* boxedTag ;
        while ((boxedTag = [e nextObject])) {
            if ([self isSelectedBoxedTag:boxedTag]) {
                [selectedTags addObject:[boxedTag text]] ;
            }
        }

        NSArray* output = [selectedTags copy] ;
        [selectedTags release] ;

        return output ;
    }

    [2] http://www.fernlightning.com/doku.php?id=randd:tcloud:start
  • I'm not sure how to answer 1 (mmalc?) because it's a rather vague
    question and lacking further detail about the design of your
    application, it's rather impossible to answer in any useful way. :-}

      I would suggest googling "mmalc bindings" - he's maintained a rather
    useful list of examples, I believe two of which demonstrate wiring up
    custom views. Perhaps spending time studying those will fit some
    pieces together for you.

    > 2.  I have heard the warnings against calling -will/
    > didChangeValueForKey without "really" changing anything in between.
    > Well, consider what happens when the user clicks in an empty part of
    > the view with no modifier keys:
    >
    > [self willChangeValueForKey:@"selectedTags"] ;
    > [[self selectedIndexSet] removeAllIndexes] ;
    > [self didChangeValueForKey:@"selectedTags"] ;
    >
    > Now, what if the user clicks there a second time?  The second -
    > removeAllIndexes will actually have no effect on selectedTags.  Am I
    > disobeying the rule and possibly setting myself up for a crash in
    > some future OS version?

      You can avoid this problem by:

    1 - Wrapping (with will/did...) the changes in -setSelectedIndexSet:
    property of your view.

    2 - Creating a "selectNone" or similar method (called when a click
    selects nothing, for example) that calls:
      [self setSelectedIndexSet:[NSIndexSet indexSet]];

    3 - ONLY modifying the -selectedIndexSet property via
    -setSelectedIndexSet: anywhere else in your code.

      This funnels everything through your property accessors and so keeps
    everything clean and in one place. Hope that helps.

    --
    I.S.
  • On 2007 Oct, 22, at 13:13, I. Savant wrote:

    > I would suggest googling "mmalc bindings" - he's maintained a rather
    > useful list of examples, I believe two of which demonstrate wiring up
    > custom views. Perhaps spending time studying those will fit some
    > pieces together for you.

    Well, not really.  mmalc's GraphicsBindings has a JoystickView which
    seems to maintain KVO by using a setValue:forKeyPath: as the last
    step in processing a mouse click.  But it's trivial because the value
    being observed is only a single value, not an array, and not
    generated on the fly.

    > You can avoid this problem by:
    >
    > 1 - Wrapping (with will/did...) the changes in -setSelectedIndexSet:
    > property of your view.
    >
    > 2 - Creating a "selectNone" or similar method (called when a click
    > selects nothing, for example) that calls:
    > [self setSelectedIndexSet:[NSIndexSet indexSet]];
    >
    > 3 - ONLY modifying the -selectedIndexSet property via
    > -setSelectedIndexSet: anywhere else in your code.
    >
    > This funnels everything through your property accessors and so keeps
    > everything clean and in one place. Hope that helps.

    Thanks, I.S.  I had considered something like that, but now we're
    adding ^even more^ code!  Let me repeat that, as is, the code in my
    original post works perfectly.  If I had not heard lectures from
    Apple on this topic, I wouldn't even be worried.  If I remember
    correctly, the exact admonition was against sending will/did change
    as consecutive statements.  At least I executed some code in between,
    which ^usually^ changes the value.  Am I going to add alot of code to
    fix something that's not broken?

    I'm reminded of a similar situation in Core Data.  For a to-many
    relationship, NSManagedObject gives me a mutable set and, If I'm not
    mistaken, I can insert/remove/replace its contents as I please.  Here
    is an accessor, which was generated by mogenerator:

    - (NSMutableSet*)gluesSet {
    return [self mutableSetValueForKey:@"glues"];
    }

    Look, it doesn't even send a willAccessValueForKey/
    didAccessValueForKey, yet I can insert/remove/change the contents of
    this set (not replacing whole thing, as you recommended), and Core
    Data observes whatever I did and it all "just works".  Is this magic
    built into Core Data only?
  • On Oct 22, 2007, at 2:46 PM, Jerry Krinock wrote:

    >> I would suggest googling "mmalc bindings" - he's maintained a rather
    >> useful list of examples, I believe two of which demonstrate wiring up
    >> custom views. Perhaps spending time studying those will fit some
    >> pieces together for you.
    >
    > Well, not really.  mmalc's GraphicsBindings has a JoystickView which
    > seems to maintain KVO by using a setValue:forKeyPath: as the last
    > step in processing a mouse click.  But it's trivial because the
    > value being observed is only a single value, not an array, and not
    > generated on the fly.
    >
    Umm, and the not-so-trivial GraphicsView which observes an array of
    Graphic objects...?

    mmalc
  • On Oct 22, 2007, at 2:46 PM, Jerry Krinock wrote:

    > Look, it doesn't even send a willAccessValueForKey/
    > didAccessValueForKey, yet I can insert/remove/change the contents of
    > this set (not replacing whole thing, as you recommended), and Core
    > Data observes whatever I did and it all "just works".  Is this magic
    > built into Core Data only?
    >
    <http://developer.apple.com/cgi-bin/search.pl?q=mutableSetValueForKey&nu
    m=10&site=default_collection
    >

    mmalc
  • On 2007 Oct, 22, at 19:29, mmalc crawford wrote:

    > Umm, and the not-so-trivial GraphicsView which observes an array of
    > Graphic objects...?

    OK, I was looking for a "control" view, so I seized on the Joystick,
    not realizing that the GraphicsView was actually a "control" in the
    sense that you use it to select objects.  In fact, it provides an
    observeable array of selected objects, which is exactly what I want.

    And, mmalc's -[GraphicsView mouseDown] does exactly what I.S.
    suggested: Instead of altering the "selected indexes" set directly,
    he makes a mutable copy then re-sets it (GraphicsView.m:384), using
    setValue:forKey:, I guess, to trigger KVO.

    So, the answer to my question is that, yes, you do have to write
    significant extra code to make the array derived from this set be an
    observeable property.  And, since no one would come out and tell me
    that my shortcut was OK, I wrote the extra code.

    Thanks for the help.

    Jerry Krinock

    I changed my set getter to return an immutable copy, but I didn't
    like having to make those mutable copies.  So, instead I replaced my
    setter with some "mutators" that are now invoked from within my -
    mouseDown.  Each mutator checks to see the current selectedIndexSet
    to see if a change is necessary, and if so, wraps the act with will/
    didChangeValue for the dependent observeable key, selectedTags.

    - (NSIndexSet*)selectedIndexSet {
        return [[_selectedIndexSet copy] autorelease] ;
    }

    - (void)selectIndex:(int)index {
        if (![_selectedIndexSet containsIndex:index]) {
            [self willChangeValueForKey:@"selectedTags"] ;
            [_selectedIndexSet addIndex:index] ;
            [self didChangeValueForKey:@"selectedTags"] ;
        }
    }

    - (void)deselectIndex:(int)index {
        if ([_selectedIndexSet containsIndex:index]) {
            [self willChangeValueForKey:@"selectedTags"] ;
            [_selectedIndexSet removeIndex:index] ;
            [self didChangeValueForKey:@"selectedTags"] ;
        }
    }

    - (void)selectIndexesInRange:(NSRange)range {
        if (![_selectedIndexSet containsIndexesInRange:range]) {
            [self willChangeValueForKey:@"selectedTags"] ;
            [_selectedIndexSet addIndexesInRange:range] ;
            [self didChangeValueForKey:@"selectedTags"] ;
        }
    }

    - (void)deselectAllIndexes {
        if ([_selectedIndexSet count]) {
            [self willChangeValueForKey:@"selectedTags"] ;
            [_selectedIndexSet removeAllIndexes] ;
            [self didChangeValueForKey:@"selectedTags"] ;
        }
    }
  • > Well, not really.  mmalc's GraphicsBindings has a JoystickView which
    > seems to maintain KVO by using a setValue:forKeyPath: as the last
    > step in processing a mouse click.  But it's trivial because the
    > value being observed is only a single value, not an array, and not
    > generated on the fly.

    For anyone looking further into this sort of app, there’s also
    AXCanvas in /Developer/Examples/Accessibility. It’s a fairly good
    showcase for what you can do with KVO, especially in the undo
    management department.

    -Ben
  • > Thanks, I.S.  I had considered something like that, but now we're
    > adding ^even more^ code!  Let me repeat that, as is, the code in my
    > original post works perfectly.  If I had not heard lectures from
    > Apple on this topic, I wouldn't even be worried.  If I remember
    > correctly, the exact admonition was against sending will/did change
    > as consecutive statements.  At least I executed some code in between,
    > which ^usually^ changes the value.  Am I going to add alot of code to
    > fix something that's not broken?

    A number of good suggestions were made.  But to answer your literal
    question, no, it's not a problem removing from an empty set.  Of
    course, some of the observers might do a little more work than they
    otherwise needed to, but this is not illegal.

    > I'm reminded of a similar situation in Core Data.  For a to-many
    > relationship, NSManagedObject gives me a mutable set and, If I'm not
    > mistaken, I can insert/remove/replace its contents as I please.  Here
    > is an accessor, which was generated by mogenerator:
    >
    > - (NSMutableSet*)gluesSet {
    > return [self mutableSetValueForKey:@"glues"];
    > }
    >
    > Look, it doesn't even send a willAccessValueForKey/
    > didAccessValueForKey, yet I can insert/remove/change the contents of
    > this set (not replacing whole thing, as you recommended), and Core
    > Data observes whatever I did and it all "just works".

    -glueSet isn't implementing a property, it's forwarding a message.
    -mutableSetValueForKey is a part of Key Value Coding.
    NSManagedObject is fully KVC/KVO compliant for all its modeled
    properties, so here KVC is doing all the work to honor Core Data's
    design requirements like -willAccessValueForKey.

    You can poke at it in gdb if you want to see the control flow.

    > Is this magic built into Core Data only?

    No, magic is just illusion by another name.
    --

    terminally curious,

    -Ben

    __________________________________________________________________________
    Ben Trumbull
    <trumbull...>                            Development Technologies
    (408) 974-5790                                      Core Data
    Change is the Epitome of Hope.                      Apple, Inc.
previous month october 2007 next month
MTWTFSS
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        
Go to today