KVC / KVO и привязки: почему я получаю только одно уведомление об изменении? - PullRequest
2 голосов
/ 21 августа 2009

Я вижу странное поведение с KVC / KVO Какао и привязками. У меня есть объект NSArrayController с его содержимым, связанным с NSMutableArray, и у меня есть контроллер, зарегистрированный в качестве наблюдателя свойства arrangedObjects в NSArrayController. С этой настройкой я ожидаю получать уведомление KVO каждый раз, когда массив изменяется. Однако, похоже, что уведомление KVO отправляется только один раз; самый первый раз, когда массив модифицируется.

Я создал совершенно новый проект «Cocoa Application» в Xcode, чтобы проиллюстрировать проблему. Вот мой код:

BindingTesterAppDelegate.h

#import <Cocoa/Cocoa.h>

@interface BindingTesterAppDelegate : NSObject <NSApplicationDelegate>
{
    NSWindow * window;
    NSArrayController * arrayController;
    NSMutableArray * mutableArray;
}
@property (assign) IBOutlet NSWindow * window;
@property (retain) NSArrayController * arrayController;
@property (retain) NSMutableArray * mutableArray;
- (void)changeArray:(id)sender;
@end

BindingTesterAppDelegate.m

#import "BindingTesterAppDelegate.h"

@implementation BindingTesterAppDelegate

@synthesize window;
@synthesize arrayController;
@synthesize mutableArray;

- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
    NSLog(@"load");

    // create the array controller and the mutable array:
    [self setArrayController:[[[NSArrayController alloc] init] autorelease]];
    [self setMutableArray:[NSMutableArray arrayWithCapacity:0]];

    // bind the arrayController to the array
    [arrayController bind:@"content" // see update
                 toObject:self
              withKeyPath:@"mutableArray"
                  options:0];

    // set up an observer for arrangedObjects
    [arrayController addObserver:self
                      forKeyPath:@"arrangedObjects"
                         options:0
                         context:nil];

    // add a button to trigger events
    NSButton * button = [[NSButton alloc]
                         initWithFrame:NSMakeRect(10, 10, 100, 30)];
    [[window contentView] addSubview:button];
    [button setTitle:@"change array"];
    [button setTarget:self];
    [button setAction:@selector(changeArray:)];
    [button release];

    NSLog(@"run");
}

- (void)changeArray:(id)sender
{
    // modify the array (being sure to post KVO notifications):
    [self willChangeValueForKey:@"mutableArray"];
    [mutableArray addObject:[NSString stringWithString:@"something"]];
    NSLog(@"changed the array: count = %d", [mutableArray count]);
    [self didChangeValueForKey:@"mutableArray"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    NSLog(@"%@ changed!", keyPath);
}

- (void)applicationWillTerminate:(NSNotification *)notification
{
    NSLog(@"stop");
    [self setMutableArray:nil];
    [self setArrayController:nil];
    NSLog(@"done");
}

@end

А вот и вывод:

load
run
changed the array: count = 1
arrangedObjects changed!
changed the array: count = 2
changed the array: count = 3
changed the array: count = 4
changed the array: count = 5
stop
arrangedObjects changed!
done

Как видите, уведомление KVO отправляется только в первый раз (и еще раз при выходе из приложения). Почему это так?

Обновление:

Спасибо orque за указание, что я должен быть привязан к contentArray моего NSArrayController, а не только к его content. Вышеуказанный код работает, как только это изменение будет сделано:

// bind the arrayController to the array
[arrayController bind:@"contentArray" // <-- the change was made here
             toObject:self
          withKeyPath:@"mutableArray"
              options:0];

Ответы [ 3 ]

7 голосов
/ 22 августа 2009

Во-первых, вы должны привязаться к contentArray (не content):

    [arrayController bind:@"contentArray"
             toObject:self
          withKeyPath:@"mutableArray"
              options:0];

Тогда простой способ - просто использовать arrayController для изменения массива:

- (void)changeArray:(id)sender
{
    // modify the array (being sure to post KVO notifications):
    [arrayController addObject:@"something"];
    NSLog(@"changed the array: count = %d", [mutableArray count]);
}

(в реальном сценарии вы, скорее всего, захотите, чтобы действие кнопки вызывало -addObject:)

Использование - [NSMutableArray addObject] не будет автоматически уведомлять контроллер. Я вижу, что вы пытались обойти это, вручную используя willChange / didChange на mutableArray. Это не будет работать, потому что сам массив не изменился. То есть, если система KVO запрашивает mutableArray до и после изменения, она все равно будет иметь один и тот же адрес.

Если вы хотите использовать - [NSMutableArray addObject], вы можете изменить / didChange для расположения объектов:

- (void)changeArray:(id)sender
{
    // modify the array (being sure to post KVO notifications):
    [arrayController willChangeValueForKey:@"arrangedObjects"];
    [mutableArray addObject:@"something"];
    NSLog(@"changed the array: count = %d", [mutableArray count]);
    [arrayController didChangeValueForKey:@"arrangedObjects"];
}

Может быть более дешевый ключ, который дал бы такой же эффект. Если у вас есть выбор, я бы порекомендовал просто работать через контроллер и оставлять уведомления вплоть до базовой системы.

5 голосов
/ 22 августа 2009

Гораздо лучший способ, чем явная публикация полноценных уведомлений KVO, - реализовать средства доступа к массиву и использовать их. Тогда КВО публикует уведомления бесплатно.

Таким образом, вместо этого:

[self willChangeValueForKey:@"things"];
[_things addObject:[NSString stringWithString:@"something"]];
[self didChangeValueForKey:@"things"];

Вы бы сделали это:

[self insertObject:[NSString stringWithString:@"something"] inThingsAtIndex:[self countOfThings]];

Мало того, что KVO опубликует для вас уведомление об изменении, но это будет более конкретное уведомление, представляющее собой изменение вставки массива, а не изменение всего массива.

Я обычно добавляю addThingsObject: метод, который делает выше, чтобы я мог сделать:

[self addThingsObject:[NSString stringWithString:@"something"]];

Обратите внимание, что add<Key>Object: в настоящее время не является KVC-распознаваемым форматом селектора для свойств массива (только для заданных свойств), тогда как insertObject:in<Key>AtIndex: есть, поэтому ваша реализация первого (если вы решите это сделать) должна используйте последнее.

0 голосов
/ 02 февраля 2016

О, я долго искал это решение! Спасибо всем ! Получив идею и поиграв, я нашел другой очень причудливый способ:

Предположим, у меня есть объект CubeFrames, подобный этому:

@interface CubeFrames : NSObject {
NSInteger   number;
NSInteger   loops;
}

My Array содержит объекты кубических фреймов, они управляются через (MVC) objectController и отображаются в табличном представлении. Привязки делаются обычным способом: «Содержимое массива» objectController привязано к моему массиву. Важно: установите «Имя класса» для objectController для класса CubeFrames

Если я добавлю таких наблюдателей в свой Appdelegate:

-(void)awakeFromNib {

//
// register ovbserver for array changes :
// the observer will observe  each item of the array when it changes:
//      + adding a cubFrames object
//      + deleting a cubFrames object
//      + changing values of loops or number in the tableview
[dataArrayCtrl addObserver:self forKeyPath:@"arrangedObjects.loops" options:0 context:nil];
[dataArrayCtrl addObserver:self forKeyPath:@"arrangedObjects.number" options:0 context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                  ofObject:(id)object
                    change:(NSDictionary *)change
                   context:(void *)context
{
    NSLog(@"%@ changed!", keyPath);
}

Теперь, действительно, я улавливаю все изменения: добавление и удаление строк, изменение циклов или числа: -)

...