monitoring changes in a local property

  • One of my viewcontrollers uses the representedObject to bind to the NSArrayController that holds all the data for the application (OS X, ARC on). I declared a local property mySelection (an NSArray) and bind it to the representedObject as follows:

        [self bind:@"mySelection" toObject:self.representedObject withKeyPath:@"selectedObjects" options:nil];

    So far so good.

    Now when the user changes mySelection,  not only my local property needs to be updated, but also some other parts in my code. So I cannot just rely on the automatically generated setter, and thus need to monitor a change in mySelection.  After some searching I came up with the following:

        [self.representedObject addObserver:self forKeyPath:@"selectedObjects"  options:NSKeyValueObservingOptionNew context: nil];

    and then:

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        if ([keyPath isEqualToString: @"selectedObjects"])
        {
          // do additional stuff
        }
        else
        {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }

    Again, this all works. Whenever the user changes the selection, observeValueForKeyPath: gets called and the "do additional stuff" gets executed.

    But I have the feeling I am over-complicating things.  From reading on this subject I get the impression the approach above seems to be more for monitoring properties in other classes, not in the owner class.

    So, is this the correct way to do this (responding to a change in a local property, or am I overlooking something very obvious?

    Thanks,

    - Koen.
  • It's ok, you can do it if you like, monitor your own properties, however if all you really want to do here is, in your own class, do something more when mySelection is changed, you can write your own setter and put it in there yourself directly.

    -(void)setMySelection:(NSArray*)mySelection
    {
    // set whatever instance variable you have for mySelection, with appropriate memory management if necessary

    // do the other stuff you want to do
    }

    you'll have to write the getter as well, you can't write one and synthesize the other any more.

    One more point, never, ever use addObserver:forKeyPath:options:context with a nil context. It's so tempting, it's such an easy habit to get into, it will get you one day and the resulting bug will make you tear your hair out for days. Set up a static for the context, use it, CHECK the context in the observeValueForKeyPath:.. call and only process if the context is yours else call super. Also, as Apple has now finally kindly allowed us to specify the context when we remove observers instead of the runtime guessing and getting it wrong, always use that version.

    On May 8, 2012, at 11:16 AM, Koen van der Drift wrote:

    > One of my viewcontrollers uses the representedObject to bind to the NSArrayController that holds all the data for the application (OS X, ARC on). I declared a local property mySelection (an NSArray) and bind it to the representedObject as follows:
    >
    > [self bind:@"mySelection" toObject:self.representedObject withKeyPath:@"selectedObjects" options:nil];
    >
    > So far so good.
    >
    > Now when the user changes mySelection,  not only my local property needs to be updated, but also some other parts in my code. So I cannot just rely on the automatically generated setter, and thus need to monitor a change in mySelection.  After some searching I came up with the following:
    >
    > [self.representedObject addObserver:self forKeyPath:@"selectedObjects"  options:NSKeyValueObservingOptionNew context: nil];
    >
    > and then:
    >
    > - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    > {
    > if ([keyPath isEqualToString: @"selectedObjects"])
    > {
    > // do additional stuff
    > }
    > else
    > {
    > [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    > }
    > }
    >
    > Again, this all works. Whenever the user changes the selection, observeValueForKeyPath: gets called and the "do additional stuff" gets executed.
    >
    > But I have the feeling I am over-complicating things.  From reading on this subject I get the impression the approach above seems to be more for monitoring properties in other classes, not in the owner class.
    >
    > So, is this the correct way to do this (responding to a change in a local property, or am I overlooking something very obvious?
    >
    > Thanks,
    >
    > - Koen.
  • On May 7, 2012, at 11:28 PM, Roland King wrote:

    > -(void)setMySelection:(NSArray*)newSelection
    > {
    > // set whatever instance variable you have for mySelection, with appropriate memory management if necessary

    I thought about that too, but how do I do that when using ARC? Can I still do this:

    [mySelection release];
    [mySelection = [newSelection copy]];

    or something along those lines?

    - Koen.
  • mySelection = [ newSelection copy ];

    is all you need.

    On May 8, 2012, at 11:38 AM, Koen van der Drift wrote:

    >
    > On May 7, 2012, at 11:28 PM, Roland King wrote:
    >
    >> -(void)setMySelection:(NSArray*)newSelection
    >> {
    >> // set whatever instance variable you have for mySelection, with appropriate memory management if necessary
    >
    >
    > I thought about that too, but how do I do that when using ARC? Can I still do this:
    >
    > [mySelection release];
    > [mySelection = [newSelection copy]];
    >
    > or something along those lines?
    >
    > - Koen.
  • Roland's right about ARC, you just need the set and copy. Just for the record, I don't think the pattern you present is safe when newSelection equals mySelection (as you release before you copy), you need something like one of the following:

    if (mySelection != newSelection)
    {
    [mySelection release];
    mySelection = [newSelection copy];
    }

    OR

    [mySelection autorelease];
    mySelection = [newSelection copy];

    OR

    oldSelection = mySelection;
    mySelection = [newSelection copy];
    [oldSelection release];

    Aaron

    On May 7, 2012, at 8:38 PM, Koen van der Drift wrote:

    >
    > On May 7, 2012, at 11:28 PM, Roland King wrote:
    >
    >> -(void)setMySelection:(NSArray*)newSelection
    >> {
    >> // set whatever instance variable you have for mySelection, with appropriate memory management if necessary
    >
    >
    > I thought about that too, but how do I do that when using ARC? Can I still do this:
    >
    > [mySelection release];
    > [mySelection = [newSelection copy]];
    >
    > or something along those lines?
    >
    > - Koen.
    >
  • On May 7, 2012, at 20:16 , Koen van der Drift wrote:

    > One of my viewcontrollers uses the representedObject to bind to the NSArrayController that holds all the data for the application (OS X, ARC on). I declared a local property mySelection (an NSArray) and bind it to the representedObject as follows:
    >
    > [self bind:@"mySelection" toObject:self.representedObject withKeyPath:@"selectedObjects" options:nil];
    >
    > So far so good.
    >
    > Now when the user changes mySelection,  not only my local property needs to be updated, but also some other parts in my code. So I cannot just rely on the automatically generated setter, and thus need to monitor a change in mySelection.  After some searching I came up with the following:
    >
    > [self.representedObject addObserver:self forKeyPath:@"selectedObjects"  options:NSKeyValueObservingOptionNew context: nil];
    >
    > and then:
    >
    > - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    > {
    > if ([keyPath isEqualToString: @"selectedObjects"])
    > {
    > // do additional stuff
    > }
    > else
    > {
    > [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    > }
    > }
    >
    > Again, this all works. Whenever the user changes the selection, observeValueForKeyPath: gets called and the "do additional stuff" gets executed.
    >
    > But I have the feeling I am over-complicating things.  From reading on this subject I get the impression the approach above seems to be more for monitoring properties in other classes, not in the owner class.
    >
    > So, is this the correct way to do this (responding to a change in a local property, or am I overlooking something very obvious?

    It's not obvious why you need a "mySelection" property at all. You just want to "do additional stuff" when the selection changes, so:

    [self.representedObject addObserver:self forKeyPath:@"selectedObjects" options:NSKeyValueObservingOptionInitial context: myContext];

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
    if (content != myContext)
      [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    else if ([keyPath isEqualToString: @"selectedObjects"])
    {
      // do additional stuff
    }
    }

    Note that NSKeyValueObservingOptionNew is not a useful option here, and you probably need NSKeyValueObservingOptionInitial to get "additional stuff" for the initial selection (which may be nil, of course, but don't assume it). Also, I added the missing context.

    Inside the view controller, if you need the actual selection, it's '[[self.representedObject] selectedObjects]'. It may be convenient to package this into a *readonly* property:

    - (NSArray*) mySelection
    {
    return [[self.representedObject] selectedObjects];
    }

    If you actually need KVO compliance for "mySelection" (that is, something else observes *it*), then add this:

    + (NSSet*) keyPathsForValuesAffectingMySelection
    {
    return [NSSet setWithObject: @"representedObject.selectedObjects"];
    }

    Don't make "mySelection" readwrite -- it's really a derived property and so should be readonly. Also, get rid of the "bind" invocation. It was never the right approach.

    P.S. Personally, I wouldn't bind to a NSArrayController like this, because it just obscures the MVC lines of your app. The array controller is getting its content from somewhere: from this view controller itself, from a window controller, or from the app delegate. Assuming the last of these (based on your description of the data as app-wide), then I'd give the app delegate a "selectionIndexes" property (of type NSIndexSet*), bind the array controller's "selectionIndexes" binding to this property in IB, and have the view controller observe the app delegate "selectionIndexes" property instead of the array controller "selectedObjects" property.

    The rationale for this is that the array controller is merely a glue object foisted on you by the bindings UI conventions, and the less your code needs to know about it the better.
  • On Tue, May 8, 2012 at 12:27 AM, Quincey Morris
    <quinceymorris...> wrote:

    > It's not obvious why you need a "mySelection" property at all.

    It's used in a master-detail setup in my application.  Briefly, my
    application has the familiar Mail layout: A column (outlineview) on
    the left with groups, then a column (tableview) in the middle showing
    the entities from a selected group, and on the right two panes which
    show details about the selected entity. One of the panes on the right
    swaps views (using the example code from Hillegass' book). So I am
    using 'mySelection' (I probably could also use 'myEntity') so the
    viewcontroller for each of these views knows what data to show. I hope
    this makes sense.

    > P.S. Personally, I wouldn't bind to a NSArrayController like this, because
    > it just obscures the MVC lines of your app. The array controller is getting
    > its content from somewhere: from this view controller itself, from a window
    > controller, or from the app delegate. Assuming the last of these (based on
    > your description of the data as app-wide), then I'd give the app delegate a
    > "selectionIndexes" property (of type NSIndexSet*), bind the array
    > controller's "selectionIndexes" binding to this property in IB, and have the
    > view controller observe the app delegate "selectionIndexes" property instead
    > of the array controller "selectedObjects" property.

    That sounds like a good idea and I will see how that fits in my app. I
    am indeed using the AppDelegate to switch the views, and pass the
    managedobjectcontext around as explained in the book by Hillegass.
    But I see now that is not really needed, since the views only show
    info about one entity. I just need to make sure that when the user
    changes the selection in the middle column, the details are updated in
    the right two panes.

    Thanks for all the input, lots of food for thought, and again highly
    appreciated.

    - Koen.
  • On May 8, 2012, at 12:27 AM, Quincey Morris wrote:

    > P.S. Personally, I wouldn't bind to a NSArrayController like this, because it just obscures the MVC lines of your app. The array controller is getting its content from somewhere: from this view controller itself, from a window controller, or from the app delegate. Assuming the last of these (based on your description of the data as app-wide), then I'd give the app delegate a "selectionIndexes" property (of type NSIndexSet*), bind the array controller's "selectionIndexes" binding to this property in IB, and have the view controller observe the app delegate "selectionIndexes" property instead of the array controller "selectedObjects" property.
    >
    > The rationale for this is that the array controller is merely a glue object foisted on you by the bindings UI conventions, and the less your code needs to know about it the better.

    Hmmm, an indexset just gives me an index, how do I get the object from it that it belongs to? I think I still need to be able to access the NSArrayController (that feeds the NSTableView) for that?

    Did I miss something?

    - Koen.
  • On May 9, 2012, at 18:48 , Koen van der Drift wrote:

    > Hmmm, an indexset just gives me an index, how do I get the object from it that it belongs to? I think I still need to be able to access the NSArrayController (that feeds the NSTableView) for that?

    The array controller is also bound to some indexed property of your app delegate, "myThings". For example, you might have (in the app delegate .h file):

    @property (readonly) NSArray* myThings;

    Then to get the selected objects:

    [appDelegate.myThings objectsAtIndexes: appDelegate.selectionIndexes];

    or to get just "the" selected object:

    appDelegate.selectionIndexes.count ? [appDelegate.myThings objectAtIndex: appDelegate.selectionIndexes.firstIndex] : nil;

    Note that what you're doing here is querying the MVC "M" (model) for objects. Getting the selected objects from the array controller is (IMO) querying the "V" -- because I think of array controllers as effectively part of the view complex: they are, after all, typically part of the NIB that defines the view components. Technically, they're controllers (mediating controllers, rather than coordinating controllers), but my attitude is that they're [semi-]private to a sub-MVC system within the main MVC "V". Others might not choose to think of them this way.

    Anyway, my point is that by querying the "M" rather than the "V", your view controller doesn't have dependencies on "V" implementation details. It doesn't need to know that there is a table view or an array controller. That's *usually* a cleaner design.

    (Sometimes it *is* easiest to query the array controller directly -- for example, if you need "selectedObjects" or "arrangedObjects" in the sorted order.)
  • On May 9, 2012, at 10:12 PM, Quincey Morris wrote:

    > The array controller is also bound to some indexed property of your app delegate, "myThings". For example, you might have (in the app delegate .h file):

    In my case, the contentset of the array controller is bound to a tree controller (which is bound to the MOC of my model).  I fail to see how I also bind it to an array.

    But to take two steps back, there can only be one item selected at once in myThings,  so I really only need to be notified which one that is when the selection changes, so that the pane can update its contents. I have another 'Info' pane where I bind the value of text fields directly to properties of self.representedObject.selection in IB (the representedObject is the arrayController) But in the other pane I cannot do that, I need to do calculations with the selected "Thing", and then display them in a table.  Which is why I initially used the bind code.

    - Koen.
  • On May 9, 2012, at 19:56 , Koen van der Drift wrote:

    > In my case, the contentset of the array controller is bound to a tree controller (which is bound to the MOC of my model).  I fail to see how I also bind it to an array.

    Well, as to a direct answer, I'm stumped.

    If the array controller is in entity mode, AFAICT there's no binding (to the data model) that would let the data model keep track of what's selected via bindings. There simply appears to be no equivalent to the "selectionIndexes" binding when in entity mode.**

    So you have 2 choices that I can see:

    1. Shrug and have your view controller monitor the array controller's "selectedObjects" directly.

    2. Have your *app delegate* monitor the array controller's "selectedObjects", and provide the result as a public KVO-compliant property. This would keep the implementation of how the selection is determined as a private detail of the app delegate, preventing the array controller dependency from spreading throughout your application design.

    ** It's possible that NSArrayController maintains "selectionIndexes" when the content is a NSOrderedSet, in Lion. In that case, you can maintain (via bindings) both a tree "selectionIndexPaths" and an ordered set "selectionIndexes" property in the app delegate, then retrieve the selected "myThings" objects via a two-step lookup process. I wouldn't bet on it working, though.
  • I'm going to re-read Chapter 31 and 32 of Hillegass' book, which more or less cover this subject (view swapping and core data relations).  Briefly what he does is is adding a MOC property to each view controller, so its views can use that.  I'll post back later.

    - Koen.

    On May 10, 2012, at 12:39 AM, Quincey Morris wrote:

    > On May 9, 2012, at 19:56 , Koen van der Drift wrote:
    >
    >> In my case, the contentset of the array controller is bound to a tree controller (which is bound to the MOC of my model).  I fail to see how I also bind it to an array.
    >
    > Well, as to a direct answer, I'm stumped.
    >
    > If the array controller is in entity mode, AFAICT there's no binding (to the data model) that would let the data model keep track of what's selected via bindings. There simply appears to be no equivalent to the "selectionIndexes" binding when in entity mode.**
    >
    > So you have 2 choices that I can see:
    >
    > 1. Shrug and have your view controller monitor the array controller's "selectedObjects" directly.
    >
    > 2. Have your *app delegate* monitor the array controller's "selectedObjects", and provide the result as a public KVO-compliant property. This would keep the implementation of how the selection is determined as a private detail of the app delegate, preventing the array controller dependency from spreading throughout your application design.
    >
    >
    > ** It's possible that NSArrayController maintains "selectionIndexes" when the content is a NSOrderedSet, in Lion. In that case, you can maintain (via bindings) both a tree "selectionIndexPaths" and an ordered set "selectionIndexes" property in the app delegate, then retrieve the selected "myThings" objects via a two-step lookup process. I wouldn't bet on it working, though.
    >
    >
previous month may 2012 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