NSArrayController not rearranging correctly

  • Hello,

      I have an NSArrayController (automaticallyRearrangesObjects = YES) on which I
    set a filterPredicate in code (not through bindings). Most of the time,
    rearranging works but in one 100% reproducible case, the controller produces an
    empty arrangedObjects array when it should produce a non-empty one.

    It always happens if a certain predicate (A) was set before the predicate that
    produces the wrong result (B) is set. If any another predicate (or none) is set
    when setting (B), arrangedObjects is correctly populated. This is completely
    reproducible.

    When I call rearrangeObjects on the array controller, the result gets rectified.
    However, even with automaticallyRearrangesObjects = NO, the array controller
    rearranges automatically so when I rearrange manually after setting the filter,
    I get two KVO change notifications for arrangedObjects which can be very costly
    and I'd really like to avoid that.

    Any ideas on how to force NSArrayController to (1) either produce a correct
    result automatically or (2) prevent to do any rearranging by itself when a
    filterPredicate is set?

    Regards
    Markus
    --
    __________________________________________
    Markus Spoettl
  • On 17 Jul 2012, at 22:41, Markus Spoettl wrote:

    > Hello,
    >
    > I have an NSArrayController (automaticallyRearrangesObjects = YES) on which I set a filterPredicate in code (not through bindings). Most of the time, rearranging works but in one 100% reproducible case, the controller produces an empty arrangedObjects array when it should produce a non-empty one.
    >
    > It always happens if a certain predicate (A) was set before the predicate that produces the wrong result (B) is set. If any another predicate (or none) is set when setting (B), arrangedObjects is correctly populated. This is completely reproducible.
    >
    > When I call rearrangeObjects on the array controller, the result gets rectified. However, even with automaticallyRearrangesObjects = NO, the array controller rearranges automatically so when I rearrange manually after setting the filter, I get two KVO change notifications for arrangedObjects which can be very costly and I'd really like to avoid that.
    >
    > Any ideas on how to force NSArrayController to (1) either produce a correct result automatically or (2) prevent to do any rearranging by itself when a filterPredicate is set?

    Try subclassing NSArrayController and providing a custom implementation of -arrangeObjects:

    Even if you do nothing but set a breakpoint and call super, it’ll give you a better idea of quite what the rearrangement is doing, and what’s triggering it.
  • On Jul 17, 2012, at 4:41 PM, Markus Spoettl wrote:

    > I have an NSArrayController (automaticallyRearrangesObjects = YES) on which I set a filterPredicate in code (not through bindings). Most of the time, rearranging works but in one 100% reproducible case, the controller produces an empty arrangedObjects array when it should produce a non-empty one.
    >
    > It always happens if a certain predicate (A) was set before the predicate that produces the wrong result (B) is set. If any another predicate (or none) is set when setting (B), arrangedObjects is correctly populated. This is completely reproducible.
    >
    > When I call rearrangeObjects on the array controller, the result gets rectified. However, even with automaticallyRearrangesObjects = NO, the array controller rearranges automatically so when I rearrange manually after setting the filter, I get two KVO change notifications for arrangedObjects which can be very costly and I'd really like to avoid that.
    >
    > Any ideas on how to force NSArrayController to (1) either produce a correct result automatically or (2) prevent to do any rearranging by itself when a filterPredicate is set?

    I would check if an exception has been thrown during the setting of the filter, thus interrupting it from rearranging the objects.  My suspicion is that you have a KVO-compliance bug that means that it can't unregister an observation.

    By the way, I would not expect automaticallyRearrangesObjects == NO to prevent rearranging when a new filter predicate is set.  That property is about whether or not the array controller watches the relevant properties of the content objects so that, if they change in a manner which would affect whether they're filtered or how they're sorted, the array controller rearranges them.  It doesn't have to do any such elaborate watching to know to rearrange its objects when the filter predicate or sort descriptors themselves change.

    Good luck,
    Ken
  • On 7/18/12 1:15 AM, Mike Abdullah wrote:
    >> When I call rearrangeObjects on the array controller, the result gets
    >> rectified. However, even with automaticallyRearrangesObjects = NO, the
    >> array controller rearranges automatically so when I rearrange manually
    >> after setting the filter, I get two KVO change notifications for
    >> arrangedObjects which can be very costly and I'd really like to avoid
    >> that.
    >>
    >> Any ideas on how to force NSArrayController to (1) either produce a correct
    >> result automatically or (2) prevent to do any rearranging by itself when a
    >> filterPredicate is set?
    >
    > Try subclassing NSArrayController and providing a custom implementation of
    > -arrangeObjects:
    >
    > Even if you do nothing but set a breakpoint and call super, it’ll give you a
    > better idea of quite what the rearrangement is doing, and what’s triggering
    > it.

    Thanks for the suggestion. Subclassing and implementing -arrangeObjects: makes
    the rearrangement issue go away. As to why I have no idea. How can that be. The
    subclass implementation does this:

    - (NSArray *)arrangeObjects:(NSArray *)objects
    {
        return [super arrangeObjects:objects];
    }

    Setting a breakpoint there shows me that -arrangeObjects is called when I call
    -setFilterPredicate: and nothing suspicious happens.

    Regards
    Markus
    --
    __________________________________________
    Markus Spoettl
  • On 7/18/12 2:14 AM, Ken Thomases wrote:
    > I would check if an exception has been thrown during the setting of the
    > filter, thus interrupting it from rearranging the objects.  My suspicion is
    > that you have a KVO-compliance bug that means that it can't unregister an
    > observation.

    That would log into the console, wouldn't it? I have no exceptions logged, and I
    have a symbolic exception breakpoint and NSKVODeallocateBreak that would fire as
    well.

    I did overwrite -arrangeObjects: to see if something suspicious happens. With
    the exception that it starts working with the subclass for no good reason, there
    are not other oddities I can see.

    > By the way, I would not expect automaticallyRearrangesObjects == NO to
    > prevent rearranging when a new filter predicate is set.  That property is
    > about whether or not the array controller watches the relevant properties of
    > the content objects so that, if they change in a manner which would affect
    > whether they're filtered or how they're sorted, the array controller
    > rearranges them.  It doesn't have to do any such elaborate watching to know
    > to rearrange its objects when the filter predicate or sort descriptors
    > themselves change.

    Thanks a lot for the explanation. From the documentation on
    automaticallyRearrangesObjects that's a little difficult to derive.

    Regards
    Markus
    --
    __________________________________________
    Markus Spoettl
  • On 7/17/12 11:41 PM, Markus Spoettl wrote:
    > I have an NSArrayController (automaticallyRearrangesObjects = YES) on which I
    > set a filterPredicate in code (not through bindings). Most of the time,
    > rearranging works but in one 100% reproducible case, the controller produces an
    > empty arrangedObjects array when it should produce a non-empty one.
    >
    > It always happens if a certain predicate (A) was set before the predicate that
    > produces the wrong result (B) is set. If any another predicate (or none) is set
    > when setting (B), arrangedObjects is correctly populated. This is completely
    > reproducible.

    And I now have a surprisingly small test project the does it as well, here's the
    source:

    http://www.shiftoption.com/temp/filtering.zip

    I started removing big portions of my app in order to rule out side effects of
    potential memory corruption elsewhere (you never know). Then I decided to try
    set up at simple test project from scratch, and what do you know, against all
    odds it worked (meaning of course it fails too).

    To recreate it, compile and run:

    Press Buttons in sequence

      1: shows "Power"
      2: shows nothing
      3: shows: "Roller Biking"
      2: shows "Power Boating" (should have shown that with the first press)

    I also looked at the predicate operator type as a potential source of the issue
    but I can't see a problem there. I'm using NSEqualToPredicateOperatorType is
    fine, as I do want a comparison using -equalTo: . NSMatchesPredicateOperatorType
    produces the correct result

    I'd be delighted if someone told me it's all my fault. Can anyone point out the
    error?

    Thanks!
    Regards
    Markus
    --
    __________________________________________
    Markus Spoettl
  • On Jul 18, 2012, at 08:52 , Markus Spoettl wrote:

    > I also looked at the predicate operator type as a potential source of the issue but I can't see a problem there. I'm using NSEqualToPredicateOperatorType is fine, as I do want a comparison using -equalTo: . NSMatchesPredicateOperatorType produces the correct result

    Oddly, the other operators (e.g. NSGreaterThanOrEqualToPredicateOperatorType) work fine. You might find a workaround using a combination of predicates using different operators.

    > I'd be delighted if someone told me it's all my fault. Can anyone point out the error?

    Maybe you're a *bit* at fault by designing your app around the assumption that there's a single, coalesced KVO notification for any change to the array controller.  There's really no API contract to that effect. You'd be better off implementing something to prevent the expensive code from running too often. (Then you'd be able to work around the apparent array controller bug by calling 'rearrangeObjects' manually.)
  • On 7/19/12 2:15 AM, Quincey Morris wrote:
    > Oddly, the other operators
    > (e.g. NSGreaterThanOrEqualToPredicateOperatorType) work fine. You might find a
    > workaround using a combination of predicates using different operators.
    >
    >> I'd be delighted if someone told me it's all my fault. Can anyone point out
    >> the error?
    >
    > Maybe you're a *bit* at fault by designing your app around the assumption that
    > there's a single, coalesced KVO notification for any change to the array
    > controller.  There's really no API contract to that effect. You'd be better off
    > implementing something to prevent the expensive code from running too often.
    > (Then you'd be able to work around the apparent array controller bug by calling
    > 'rearrangeObjects' manually.)

    That doesn't sound right. The array controller is used by views directly, they
    bind to arrangedObjects (like in the example). Since when is it recommended to
    insert an intermediate object between table view and NSArrayController? Makes
    the whole binding idea rather useless, or am I missing something?

    Regards
    Markus
    --
    __________________________________________
    Markus Spoettl
  • On Jul 18, 2012, at 23:22 , Markus Spoettl wrote:

    > That doesn't sound right. The array controller is used by views directly, they bind to arrangedObjects (like in the example). Since when is it recommended to insert an intermediate object between table view and NSArrayController? Makes the whole binding idea rather useless, or am I missing something?

    I wasn't thinking about inserting an intermediate object there. Rather, I was thinking of two techniques (and I am sure there are plenty of others) for preventing KVO operations from triggering unwanted expensive updates:

    1. Use the cancelPerform/performSelector…afterDelay:0 pattern to defer the expensive operation until the next run loop iteration AND prevent it from being queued more than once.

    2. Use a brute force flag when setting the predicate, something like this:

    dontStartExpensiveUpdate = YES;
    [_arrayController setPredicate: …];
    [_arrayController rearrangeObjects];
    // at this point, the KVO notification has been sent up to twice, but ignored each time
    dontStartExpensiveUpdate = NO;
    … do the expensive update now …

    Suggestion #1 would coalesce all rearrangements during a single run loop iteration (including multiple content changes). Suggestion #2 would just work around the rearrangement behavior.
  • On Jul 18, 2012, at 23:22 , Markus Spoettl wrote:

    > The array controller is used by views directly, they bind to arrangedObjects (like in the example). Since when is it recommended to insert an intermediate object between table view and NSArrayController?

    Ugh, I didn't pay enough attention to what you actually said. Yes, you can't just prevent the KVO notifications getting through when you're using bindings.

    So let me rephrase and see if I can say something sensible…

    To use bindings at all, you're basically buying into the idea that your bound view can reasonably respond to each KVO notification individually (from the data model, via the binding). You have no real control over the timing and granularity** and [non]redundancy of the KVO notifications. If the response is very expensive, bindings are not a reasonable solution.

    If the response to the notifications is simply to redraw, and redrawing is expensive, one way out is to move the expensive calculation into a background thread, leaving the redrawing itself to be quite cheap. This is the sort of "concurrent user interface" approach that's more usual in iOS apps, with the effect that content "pops" into view asynchronously. (There's a session on this in the 2012 WWDC videos.)

    Another way out is to bind the view to an intermediate object, such as a window controller, which is more careful about when it triggers the KVO notifications that affect the view.

    A third way is to use a data source/control action approach, instead of using bindings. This is a perfectly fine alternative, and is *better* than bindings when you need a more controlled form of communication between the source and the view.

    In the latter two approaches, you might end up asking yourself if using array controllers at all makes sense. It's easy to end up writing more code working around array controller assumptions than just writing glue code that really works. The main benefits of array controllers are (a) sorting; (b) filtering; and (c) oversight of the NSEditor/NSEditorRegistration protocols. Chances are you can do those things yourself without much trouble.

    FWIW

    ** For example, it would be perfectly legal, when you change the array controller's filter predicate, for there to be pairs of KVO notifications, one specifying just object deletions and one specifying just object insertions. If you've been getting a single generic "property changed" notification, that's almost more by luck than by design.
  • On 7/19/12 8:40 AM, Quincey Morris wrote:
    > 2. Use a brute force flag when setting the predicate, something like this:
    >
    > dontStartExpensiveUpdate = YES;
    > [_arrayController setPredicate: …];
    > [_arrayController rearrangeObjects];
    > // at this point, the KVO notification has been sent up to twice, but ignored
    > each time
    > dontStartExpensiveUpdate = NO;
    > … do the expensive update now …

    I ended up doing something similar: I subclassed NSArrayController and added a
    flag there. The heavy lifting custom views, which are bound to arrangedObjects,
    now check if the object they are bound to is my NSArrayController subclass when
    a KVO notification is delivered. If so and the flag is set, they ignore the
    observation notification. My NSArrayController subclass implements
    -rearrangeObjects which clears the flag first, then calls super.

    Of course I sacrifice a little of my perfect view class encapsulation, but it's
    justifiable because it makes solving the issue so simple. If only that
    NSArrayController bug didn't exist…

    Thanks Quincey for the elaborate responses and for confirming the bug.

    Regards
    Markus--
    __________________________________________
    Markus Spoettl
previous month july 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