第一次尝试实现键值观察,得到一些错误

时间:2014-08-30 22:27:47

标签: ios objective-c nsmutablearray key-value-observing

在尝试遵循MVC模式时,我无法实现键值观察器。我有一个控制器类,一个模型类和一个视图类。我从控制器类更新我的模型,我想在我的视图类中放置一个键值观察器,以监视NSMutableArray何时在模型中更改(如通过addObject),然后自动重绘自身。我在这篇帖子中使用了答案来指导我:How to add observer on NSMutableArray?

到目前为止

代码:

从我的场景(如果重要的话,使用精灵套件)。字母的设置将从Ctrl类完成,这只是为了测试。

BarCtrl *barCtrl = [[BarCtrl alloc] init];
BarModel *barModel = [[BarModel alloc] init];
BarView *barView = [[BarView alloc] init];

barCtrl.barModel = barModel;
barCtrl.barView = barView;
barView.barModel = barModel;

ScrabbleDeck *sd = [[ScrabbleDeck alloc] init];

if([barModel addLetter:[sd getLetter] onSide:BarModelSideRight])
    NSLog(@"Added letter");

BarModel.h

#import <Foundation/Foundation.h>
#import "Letter.h"

typedef NS_ENUM(int, BarModelSide) {
    BarModelSideLeft,
    BarModelSideRight
};

@interface BarModel : NSObject

@property (nonatomic, strong) NSMutableArray *addedLetters;

- (instancetype)init;
- (BOOL) addLetter: (Letter*) letter onSide: (BarModelSide) side;
@end

BarModel.m

#import "BarModel.h"

@interface BarModel ()

@property (nonatomic) int capacity;
@end

@implementation BarModel

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.capacity = letterCapacity;
        _addedLetters = [[NSMutableArray alloc] init];
    }
    return self;
}

//  We'll use automatic notifications for this example
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"arrayLetter"]) {
        return YES;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

- (BOOL) addLetter: (Letter*) letter onSide: (BarModelSide) side{
    if([_addedLetters count] > _capacity){
        return FALSE;
    }

    switch (side) {
        case BarModelSideLeft:
            [_addedLetters insertObject:letter atIndex:0];
            return TRUE;
            break;
        case BarModelSideRight:
            [_addedLetters addObject:letter];
            return TRUE;
            break;

        default:
            return FALSE;
            break;
    }
}

//  These methods enable KVC compliance
- (void)insertObject:(id)object inDataAtIndex:(NSUInteger)index
{
    self.addedLetters[index] = object;
}

- (void)removeObjectFromDataAtIndex:(NSUInteger)index
{
    [self.addedLetters removeObjectAtIndex:index];
}

- (id)objectInDataAtIndex:(NSUInteger)index
{
    return self.addedLetters[index];
}

- (NSArray *)dataAtIndexes:(NSIndexSet *)indexes
{
    return [self.addedLetters objectsAtIndexes:indexes];
}

- (NSUInteger)countOfData
{
    return [self.addedLetters count];
}
@end

BarView.h

#import <SpriteKit/SpriteKit.h>
#import "BarModel.h"

@interface BarView : SKSpriteNode

@property (nonatomic, strong) BarModel *barModel;

@end

BarView.m

#import "BarView.h"

@implementation BarView

static char MyObservationContext;

- (instancetype)init
{
    self = [super init];
    if (self) {
        //_barModel = [[BarModel alloc] init];

    }
    return self;
}

-(void)setBarModel:(BarModel *)barModel{

    if(_barModel != barModel)
        _barModel = barModel;

    [_barModel addObserver:self
                forKeyPath:@"arrayLetter"
                   options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
                   context:&MyObservationContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    //  Check if our class, rather than superclass or someone else, added as observer
    if (context == &MyObservationContext) {
        //  Check that the key path is what we want
        if ([keyPath isEqualToString:@"arrayLetter"]) {
            //  Verify we're observing the correct object
            if (object == self.barModel) {
                [self draw:change];
            }
        }
    }
    else {
        //  Otherwise, call up to superclass implementation
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void) draw: (NSDictionary*) change{
    NSLog(@"KVO for our container property, change dictionary is %@", change);
}
@end

当我这样做时,我得到了这个“错误”:

2014-08-31 00:23:02.828 Testing[329:60b] Added letter
2014-08-31 00:23:02.830 Testing[329:60b] An instance 0x17803d340 of class BarModel was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debugger. Here's the current observation info:
<NSKeyValueObservationInfo 0x17804eb50> (
<NSKeyValueObservance 0x1780cf180: Observer: 0x178111670, Key path: arrayLetter, Options: <New: YES, Old: YES, Prior: NO> Context: 0x100101428, Property: 0x17804eb80>

我试图按照错误的说明操作但无法找到设置断点的位置。请帮我解决这个问题!

2 个答案:

答案 0 :(得分:1)

该错误非常具有描述性。您添加self作为BarModel对象的观察者。在某些时候,该对象被取消分配。但是,您永远不会通过调用self删除removeObserver:forKeyPath:context:作为观察者。你需要这样做。

首先,在setBarModel中,确保删除self作为_barModel之前值的观察者。

接下来,您可能需要添加一个执行相同操作的dealloc方法。

答案 1 :(得分:0)

代码存在多个问题。除了Tom Harrington关于未能删除观察的特定错误所说的内容:

您为名为&#34; data&#34;的(不存在的)属性实现了索引集合访问器。也就是说,他们有&#34;数据&#34;在他们的名字中,属性名称应该是。

索引集合属性为addedLetters。因此,索引集合变异访问器应该是:

- (void)insertObject:(id)object inAddedLettersAtIndex:(NSUInteger)index;
- (void)removeObjectFromAddedLettersAtIndex:(NSUInteger)index;

您并不真正需要非变异访问器,因为您有一个具有普通getter的数组类型公共属性(即-addedLetters)。

顺便说一下,该属性的类型为NSMutableArray,它不应该是。该属性应为NSArray类型,由类型为NSMutableArray的实例变量提供支持。也就是说,类型(与属性相对)的可变性不应通过公共接口公开。当你这样做时,你必须手动声明实例变量(因为它应该与属性的类型不同,自动合成将错误),使属性copy而不是strong,并自己实现setter来执行传入的不可变数组的可变副本:

- (void) setAddedLetters:(NSArray*)addedLetters
{
    if (addedLetters != _addedLetters)
        _addedLetters = [addedLetters mutableCopy];
}

一旦您使用正确的名称实现了索引集合变异访问器, 必须仅使用那些 方法来改变集合(在初始化之后)。特别是,您的-addLetter:onSide:方法不能直接对_addedLetters实例变量进行操作。 是使该属性符合KVO类的部分。仅仅存在索引集合变异访问器并没有帮助。它们必须用于所有实际的突变。

+automaticallyNotifiesObserversForKey:的实施是多余的。自动通知是默认通知。

BarView类是观察关键路径的关键值&#34; arrayLetter&#34;在_barModel对象上,但这不是BarModel上的属性名称。我想你的意思是使用关键路径&#34; addedLetters&#34;。

最后,为了正确遵守MVC设计,您的视图不应该引用您的模型。它应该有一个控制器的参考。控制器可以将模型反映到视图中(或者,理论上,将不同内部设计的模型适应视图所期望的)。或者,在更传统的非KVO方法中,控制器实际上会告知视图何时发生了变化,并为其提供应显示的更新数据。

因此,您的控制器可以公开自己的addedLetters属性:

@property (readonly, copy, nonatomic) NSArray* addedLetters;

它可以作为派生属性实现,转发到_barModel对象:

+ (NSSet*)keyPathsForValuesAffectingAddedLetters
{
    return [NSSet setWithObject:@"barModel.addedLetters"];
}
- (NSArray*)addedLetters
{
    return self.barModel.addedLetters;
}

然后,视图将引用控制器而不是模型,它将键值观察&#34; addedLetters&#34;控制器上的关键路径。