0

I'd like to implement KVO for an NSArray property that is declared as readonly. The getter for this readonly property returns a copy of the private NSMutableArray that backs the backs the public readonly one:

In my .h:

@interface MyClass : NSObject
@property (readonly, nonatomic) NSArray *myArray;
- (void)addObjectToMyArray:(NSObject *)obj;
- (void)removeObjectFromMyArray:(NSObject *)obj;
@end

And in my .m:

@interface MyClass()
@property (strong, nonatomic) NSMutableArray *myPrivateArray;
@end

@implementation MyClass

- (NSArray *)myArray {
    return (NSArray *)[self.myPrivateArray copy];
}

- (void) addObjectToMyArray:(NSObject *)obj {
    [self willChangeValueForKey:@"myArray"];
    [self.myPrivateArray addObject:obj];
    [self didChangeValueForKey:@"myArray"];
}

- (void) removeObjectToMyArray:(NSObject *)obj {
    [self willChangeValueForKey:@"myArray"];
    [self.myPrivateArray removeObject:obj];
    [self didChangeValueForKey:@"myArray"];
}
@end

In my tests, I am seeing an exception thrown when I call didChangeValueForKey:. Is this the correct way to do this?

Bibs
  • 995
  • 1
  • 8
  • 17
  • what does the exception say? – Michael May 01 '15 at 15:08
  • why is myArray atomic and myPrivateArray nonatomic? if myPrivateArray is nonatomic and you don't synchronize anything, myArray will be not threadsafe, so it will be a nonatomic property that is violating its atomic contract. that's not the cause of the exception though.. are you accessing MyClass from different threads? Do you get EXC_BAD_ACCESS errors because of that? – Michael May 01 '15 at 15:12
  • Sorry, both are declared `nonatomic`. – Bibs May 01 '15 at 15:20
  • yes... at runtime this doesn't change anything anyways in this case. and it also tells me that you are not posting the real code here. can you reproduce your issue with the example code? No? How can you expect others to find the problem in the example code, if the example code has no problems? if you don't want to post the real code, but only example code, that's okay, but you have to make sure that the example code exhibits the same problem that you are trying to solve. – Michael May 01 '15 at 15:24

4 Answers4

6

I recommend that you don't use a separate property for the mutable array. Instead, have the array property backed by a mutable array variable. Then, implement the indexed collection mutating accessors and make all changes to the array through those. KVO knows to hook into those accessors and emit change notifications. In fact, it can emit better, more specific change notifications that can allow observers to be more efficient in how they respond.

@interface MyClass : NSObject
@property (readonly, copy, nonatomic) NSArray *myArray;
- (void)addObjectToMyArray:(NSObject *)obj;
- (void)removeObjectFromMyArray:(NSObject *)obj;
@end

@interface MyClass()
// Optional, if you want to be able to do self.myArray = <whatever> in your implementation
@property (readwrite, copy, nonatomic) NSArray *myArray;
@end

@implementation MyClass
{
    NSMutableArray *_myArray;
}

@synthesize myArray = _myArray;

// If you optionally re-declared the property read-write internally, above
- (void) setMyArray:(NSArray*)array {
    if (array != _myArray) {
        _myArray = [array mutableCopy];
    }
}

- (void) insertObject:(id)anObject inMyArrayAtIndex:(NSUInteger)index {
    [_myArray insertObject:anObject atIndex:index];
}

- (void) removeObjectFromMyArrayAtIndex:(NSUInteger)index {
    [_myArray removeObjectAtIndex:index];
}

- (void) addObjectToMyArray:(NSObject *)obj {
    [self insertObject:obj inMyArrayAtIndex:_myArray.count];
}

- (void) removeObjectToMyArray:(NSObject *)obj {
    NSUInteger index = [_myArray indexOfObject:obj];
    if (index != NSNotFound)
        [self removeObjectFromMyArrayAtIndex:index];
}
@end
Ken Thomases
  • 88,520
  • 7
  • 116
  • 154
  • Thanks Ken. If you wouldn't mind, could you explain what exactly is happening here? – Bibs May 01 '15 at 17:23
  • @KenThomases, when I run my tests I get an `EXC_BAD_ACCESS` exception for method `addObjectToMyArray`. Any idea what could be causing this? – Bibs May 01 '15 at 18:04
  • 1
    A property declaration establishes *interface* (e.g. what accessor methods a client can expect). It doesn't force a particular *implementation*. So, just because a property is declared to be of type `NSArray*`, that doesn't mean that its backing storage has to be an `NSArray*`. In this case, because `NSMutableArray` is-a `NSArray`, it's convenient to use an `NSMutableArray` as the backing storage for the property, without the need to have two interrelated properties. With one property, you just need to make sure it's KVO-compliant, which is most easily done by always mutating it via accessors. – Ken Thomases May 01 '15 at 21:12
  • Regarding your exception, you probably failed to unregister an observer before it was deallocated. So, KVO is trying to call `-observeValueForKeyPath:...` on a no-longer-existing object. Or it may be a different but similar bug. Run with the Zombies instrument. If you need more help, show the details of the crash as others have asked. – Ken Thomases May 01 '15 at 21:15
0

According to the KVO docs, https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-BAJEAIEE, you need to implement automaticallyNotifiesObserversForKey, something like

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {

BOOL automatic = NO;
if ([theKey isEqualToString:@"myArray"]) {
    automatic = NO;
}
else {
    automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;

}

I have not tested this code, so apologies if I am on the wrong track.

Lev Landau
  • 788
  • 3
  • 16
0

I don't have terribly much experience with this, but I post this answer in hopes that it will either solve your issue or lead you to a solution. In the past I've used this:

-(void)viewDidLoad{
    [self addObserver:self forKeyPath:kYoutubeObserverKey options:NSKeyValueObservingOptionNew context:nil];
}

-(void) addLocatedYoutubeURLToList:(NSString *)youtubeURL{

    // -- KVO Update of Youtube Links -- //
    [self willChangeValueForKey:kYoutubeObserverKey
                withSetMutation:NSKeyValueUnionSetMutation
                   usingObjects:[NSSet setWithObject:youtubeURL]];

    [self.youtubeLinksSet addObject:youtubeURL];

    [self didChangeValueForKey:kYoutubeObserverKey
               withSetMutation:NSKeyValueUnionSetMutation
                  usingObjects:[NSSet setWithObject:youtubeURL]];
}

kYoutubeObserverKey corresponds to:

static NSString * const kYoutubeObserverKey = @"youtubeLinksSet";

and I use a property of the same name in this class, hence the keyvalue name:

@property (strong, nonatomic) NSMutableSet * youtubeLinksSet;

I would add an observer for your key and specify what change your interested in observing. Additionally, I'd keep your key naming consistent, meaning that if you're updating the private key, then observe that private key, not the public one. When the observer detects a change in the private key, then have your public key update as a result of that. For example:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{

    NSNumber * keyValueChangeType = change[@"kind"];
    if ([keyValueChangeType integerValue] == NSKeyValueChangeInsertion) {

        if ([keyPath isEqualToString:kYoutubeObserverKey] ) {
            //code and such...
        }
    }
}
Louis Tur
  • 1,303
  • 10
  • 16
0

This is fragile, and - (NSArray *)myArray keeps returning different values for the same array which KVO doe not like.

You'd be better of to define a private mutable array and a public read-only array. The when you make changes to the mutable array:

self.myPublicReadOnlyArray=self.myMutableArray.copy;

That way you can avoid all the will/has changed notifications because self.myPublicReadOnlyArray is KVC/KVO compliant.

Gerd K
  • 2,933
  • 1
  • 20
  • 23