8

I have class A, with instance variables deriving attributes from cached data, this cached data is represented as a singleton and is part of A (it's essentially a NSDictionary). From time to time, this cache is modified.

When this happens, I would like to have all A instances pull new information from the cache the next time they access their attribute , or in other words, invalidate their attribute content.

Up to now, each instances is notified manually (using a static array to track members). I'm not a fan. There's might be an option with notification centre, but I'd rather try with KVO.

Has anyone ever managed to subscribe to KVO changes from a class variable on iOS ? (in other words, use changes from a static variable of A to tell A-instances to refresh their data.)

in otherwords, I'd love to have

static void* context = &context;
static myHadHocObject* msignal;

and later in Class A code

[msignal addObserver:self forKeyPath:@"operation" options:NSKeyValueObservingOptionNew context:context];

and be notified of msignal changes in a class instance via

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

}

I've tried to use various had hoc classes, with no luck. Seems I'm missing something.

pointers are welcome, thanks !

Alex
  • 1,581
  • 1
  • 11
  • 27
  • Are you able to observe changes on that object if it's not static? Making it static shouldn't affect KVO, so I think it's more likely that the event is never fired. – chedabob May 23 '15 at 13:42
  • Objective-C doesn't have "class variables". – user102008 May 23 '15 at 19:38
  • @user102008 dito. made changes for static. chedabob, you're right, but for some reasons, I couldn't see where the problem was. meligaletiko suggested a much cleaner approach than the one I used. – Alex May 23 '15 at 20:43

2 Answers2

3

KVO says that a specific object observes changes on a property of another object (or itself). So i think what you're trying to achieve is not possible, at least not like this. Or at least you need to go deeper on the KVO mechanism.

You can get all information you need to answer your question from Apple Key-Value Observing Programming Guide

Unlike notifications that use NSNotificationCenter, there is no central object that provides change notification for all observers. Instead, notifications are sent directly to the observing objects when changes are made. NSObject provides this base implementation of key-value observing, and you should rarely need to override these methods.

You can fire KVO, with - willChangeValueForKey: and - didChangeValueForKey:. You can read more about this at NSKeyValueObserving Protocol Reference

I suggest that you use a different approach, by creating a manager to manage your cache with and observe the values on that cache, just an example.

CacheManager.h

#import <Foundation/Foundation.h>

@interface CacheManager : NSObject

@property (nonatomic, strong, readonly) NSArray *data;

+ (instancetype)sharedManager;

@end

CacheManager.m

#import "CacheManager.h"

@implementation CacheManager

- (instancetype)init {
    if (self = [super init]) {
        _data = [[NSArray alloc] init];
    }

    return self;
}

+ (instancetype)sharedManager {
    static CacheManager *selfManager;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        selfManager = [[[self class] alloc] init];
    });

    return selfManager;
}

@end

ViewController.m

#import "ViewController.h"

#import "CacheManager.h"

static void *CacheManagerDataChangedContext = &CacheManagerDataChangedContext;

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    CacheManager *cacheManager = [CacheManager sharedManager];

    [cacheManager addObserver:self forKeyPath:NSStringFromSelector(@selector(data)) options:NSKeyValueObservingOptionNew context:CacheManagerDataChangedContext];
}

- (void)dealloc {
    [[CacheManager sharedManager] removeObserver:self forKeyPath:NSStringFromSelector(@selector(data)) context:CacheManagerDataChangedContext];
}

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

    if (context == CacheManagerDataChangedContext) {
        <# your stuff #>
    }

    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end

If you observe the data property of CacheManager from all the other instances, all that instance will get notified when that changes.

Hope that helps ;)

portella
  • 833
  • 6
  • 14
  • thanks a lot ! I have a cache manager but clearly I missed the point of using the context handle to notifiy the appropriate objects. Very good suggestion, thanks ! – Alex May 23 '15 at 14:04
  • mmm.. the observer is 'self' and the receiver is cacheManger, no ? – Alex May 23 '15 at 17:34
  • Yes you're right, sorry. it should be `[cacheManager addObserver:self forKeyPath:NSStringFromSelector(@selector(data)) options:NSKeyValueObservingOptionNew context:CacheManagerDataChangedContext];` – portella May 23 '15 at 17:37
  • for sake of completeness, I had to change @property (nonatomic, strong, readonly) NSArray *data; to @property (nonatomic, strong) NSString *data; like this, when doing anything like [CacheManager sharedManager].data = @"foobarr"; all instances are notified and their attribute tagged as dirty in - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context . Works well, and is much sturdier than tracking class instances !!! – Alex May 23 '15 at 18:35
1

Just in case, and without diminishing the merits of @meligaletiko, I'm writing the blue print of the full solution, corresponding to my problem, in case it can be useful to anyone.

Problem was: given class instances using a copy of data fetched from a store, how to efficiently notify all class instances when one of them modifies data ? (ex: data comes from NSUserDefaults, but you don't necessarily want to fetch/save data from/to it, each time, because it's slow) Also, data changes are unfrequent.

from @meligaletiko

#import <Foundation/Foundation.h>

@interface CacheManager : NSObject

@property (nonatomic, strong) NSString *data;

+ (instancetype)sharedManager;

@end

#import "CacheManager.h"

@implementation CacheManager

- (instancetype)init {
    if (self = [super init]) {
        _data = @"x";
    }

    return self;
}

+ (instancetype)sharedManager {
    static CacheManager *selfManager;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        selfManager = [[[self class] alloc] init];
    });

    return selfManager;
}

@end


static void *CacheManagerDataChangedContext = &CacheManagerDataChangedContext;
@interface A:NSObject
-(NSString*)sz;
-(void)setSz:(NSString*)sz;
@end

@interface A ()
@property (nonatomic,copy) NSString* sz;
@property (nonatomic) BOOL shouldFetchDataFromStore;

-(id)fetchDataForKey:(NSString*)key
@end

@implementation A

-(instancetype)init{
    self = [super init];
    if (self) {
        [[CacheManager sharedManager] addObserver:self forKeyPath:NSStringFromSelector(@selector(data))
                                          options:NSKeyValueObservingOptionNew
                                          context:CacheManagerDataChangedContext];
    }
    return self;
}

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

    if (context == CacheManagerDataChangedContext) {
        self.shouldFetchDataFromStore = YES;
    }

    else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
-(void)dealloc{
    [[CacheManager sharedManager] removeObserver:self forKeyPath:NSStringFromSelector(@selector(data)) context:CacheManagerDataChangedContext];
}

-(void)setSz:(NSString*)sz{
    _sz = sz;//sanity checks spared...
    [CacheManager sharedManager].data = @"x";//this line will notify all other instances

}
-(NSString*)sz{
    if(self.shouldFetchDataFromStore) { // in other class instances, this was updated to YES, thanks to KVO
        self.shouldFetchDataFromStore = NO;
        _sz = (NSString*)[self fetchDataForKey:(NSString*)sz];//updates _sz with the correct value.
    }
    return _sz;
}

@end
Cœur
  • 37,241
  • 25
  • 195
  • 267
Alex
  • 1,581
  • 1
  • 11
  • 27
  • Oh, i forgot the the `removeObserver` on `dealloc`. It looks a newbie error :( sorry. Why you don't use the manager to handle the set of the array and its manipulation? Usually i hate to have **readwrite properties** on manager, most of the time they are only manipulated by the manager. But well, this is your implementation. Well done ;) – portella May 23 '15 at 20:10
  • no worries, I added the dealloc anyway. Here, the manager acts as a small, self-contained notification centre, and doesn't know about the manipulation of the cache. This also enable the reuse of the manager for a different cache with different rules for updates/inserts etc... Thanks for suggesting this approach, was very helpful ! And it works like a charm ! – Alex May 23 '15 at 20:18