Key-Value Observer not changing on its own property - cocoa-touch

I am trying to get notifications for when a property called "currentTopViewPosition" changes. I used the following code to register for the changes and receive them:
[self addObserver:self
forKeyPath:#"currentTopViewPosition"
options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionPrior
context:NULL];
Then the receiving side:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(#"Key Path: %#\n change: %#",keyPath, change);
}
But nothing was getting logged for it.
I tested to make sure the value was actually changing by using an NSTimer to print out its value every 5ms and it was changing.
I've never seemed to get Key-value observing to work, so am I doing something wrong? missing a step?
Thanks!

The easiest way to make your property is to redeclare the property as readwrite inside your implementation file.
#property (nonatomic, readwrite, assign) ECSlidingViewControllerTopViewPosition currentTopViewPosition;
Then when setting the value, make sure you use the property setter. E.g. self.currentTopViewPosition = 1
If you are manually setting the value using an ivar directly, you will have to generate the KVO calls manually. Like this:
[self willChangeValueForKey:#"currentTopViewPosition"];
_currentTopViewPosition = 1;
[self didChangeValueForKey:#"currentTopViewPosition"];

Related

Dirty flags on Realm objects

Can anyone suggest a good pattern for implementing a dirty flag on Realm objects? Specifically, I would like every subclass of Realm Object to expose an isDirty flag that gets set whenever an instance of the class is modified and is cleared whenever the instance is written to the cloud (not the Realm). I'm working in Objective-C.
Possible solutions I can think of include the following:
Write a custom setter for every property of every objects. Set isDirty within each of those setters. Not very desirable.
Use KVO in some way. Two problems with this approach: (a) I don't fully understand how to implement this approach, and (b) Realm doesn't support KVO for managed objects (which are exactly the objects I need it for).
Use Realm notifications. Again, I don't have experience with these, and I'm not sure how to use them for this purpose.
Short of simply having a non-managed isDirty property that you manually set after performing each write transaction, KVO would be the best way to go.
Setting custom setters would indeed be incredibly messy. You'd have to have a separate one for each property you wanted to track.
Realm notifications would only work if you were tracking a set of objects and wanted to be alerted if any were changed (using collection notifications) or if anything in the Realm changed.
With KVO, you could potentially get your object subclass itself to add observers to all of its properties, which are then channeled to one method whenever any of them change, this could then be used to mark the isDirty property.
#interface MyObject: RLMObject
#property NSString *name;
#property NSInteger age;
#property BOOL isDirty;
- (void)startObserving;
- (void)stopObserving;
#end
#implementation MyObject
- (void)startObserving
{
NSArray *properties = self.objectSchema.properties;
for (RLMProperty *property in properties) {
[self addObserver:self forKeyPath:property.name options:NSKeyValueObservingOptionNew context:nil];
}
}
- (void)stopObserving
{
NSArray *properties = self.objectSchema.properties;
for (RLMProperty *property in properties) {
[self removeObserver:self forKeyPath:property.name];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
self.isDirty = YES;
}
+ (NSArray *)ignoredProperties {
return #[#"isDirty"];
}
#end
Obviously you'd want to do more checking in here than I've done (to make sure isDirty truly needs to be set), but this should give you an idea.
There's no real way to automatically know when a managed Realm object has been created, so it would be best for you to manually start and stop observing as you need it.

Raise an event whenever a property value gets changed

I have a class having 2 properties:
#property (copy) NSString *name; //name of the file
#property (copy) NSString *path; //path of the file
#property NSImage *img; //icon of the file
Problem
I want to name and img auto fill whenever path is changed like an event should be raised whenever the path is changed so that I can refresh name and img.
Many thanks in advance. Please note that I am 2 months new to objective-c and I was in the .net world from the last 2 years.
You can go with a well established KVO (Key-Value Observing) technique:
Step 1 : Assuming you already have a model class say MyData with these properties, set your controller class or where you are creating that model object as observer for path something like this:
MyData *data = [[MyData alloc] init];
[data addObserver:self forKeyPath:#"path" options:NSKeyValueObservingOptionNew context:NULL];
Step 2 : Implement observeValueForKeyPath:ofObject:change:context: method to listen to any change in the path property. Something like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
MyData *myData = (MyData *)object;
myData.name = #"My Name";
myData.image = // Set your image here
}
Step 3 : Importantly, do not forget to remove yourself as observer of this property whenever your object is being deallocated. Otherwise you may end up crashing!
(Updated to prevent looping)
You could use a setter function. Instead of using MyClass.name = ... declare a function in that class and use that to change the value:
- (void) setPath:(NSString*)newvalue{
_path = newvalue;
[self updateNameAndPicture];
}
Then, when you change it, use
[InstanceOfMyClass setPath:#"/new/path/"];

How to trampoline from a Cocoa Notification to the invocation of c function pointer?

I want to set up something like this in my environment.
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self
forKeyPath:[#"values." stringByAppendingString: #"MyPreference"]
options:NSKeyValueObservingOptionNew
context:NULL];
I'm doing this from a Smalltalk environment. The particular Smalltalk can actually drive the above thru the objective-c runtime features. The problem is the "self" there as the addObserver: argument. From Smalltalk, I can create an C function pointer that will act as a callback into Smalltalk. But it can't offer it's notion of what an Object is to the ObjectiveC environment.
So I'm trying to figure out how to trampoline from some sort of object with the appropriate API that causes the function pointer I create to be executed. I've looked at NSInvokation and friends, but none of those are used to wrap around a C function pointer. And I'm not sure they'd translate the addObserver:forKeyPath:options:context: to do the right thing.
Boiled down the question is, how do you use existing Cocoa objects to register a response to a Notification that is the execution of a C function pointer without compiling any new objc code.
You'll need to create a glue class in Objective-C. You could do this:
typedef void TravisGriggsCallback();
#interface TravisGriggsGlue {
TravisGriggsCallback *_callback;
}
- (id)initWithCallback:(TravisGriggsCallback *)callback;
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
#end
#implementation TravisGriggsGlue
- (id)initWithCallback:(TravisGriggsCallback *)callback
{
if (!(self = [super init]))
return nil;
_callback = callback;
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
_callback();
}
#end
If you need to pass arguments to the callback, you'll need to add instance variables to hold the arguments, and pass them to the init method.
Then you use it like this:
TravisGriggsGlue *glue = [[TravisGriggsGlue alloc]
initWithCallback:&someCallbackFunction];
[[NSUserDefaultsController sharedUserDefaultsController]
addObserver:self
forKeyPath:[#"values." stringByAppendingString: #"MyPreference"]
options:NSKeyValueObservingOptionNew
context:NULL];
[glue release]; // only needed if not using ARC
I don't know which kind of Smalltalk you're using, but in pharo/squeak you are probably using the ObjectiveCBridge plugin. If that is the case, you can extend an ObjectiveCSqueakProxy and use it as an object delegated from cocoa to your application.
hope this works for you :)
use a simple function object:
#interface MONFunctor : NSObject
- (void)performFunction;
#end
Then just specialize as needed:
#interface MONFunctorSpecialization : NSObject
{
#private
// data, state, arguments for function, function pointer...
}
- (void)performFunction;
#end
You could use the functor as a listener, but I sketched this out as though your listener would just say [functor performFunction]; in the callback.
In retrospect, there's no need for MONFunctor to be a class; a protocol would suffice.

KVO With NSMutableArray

I have a NSMutableArray property in my AppDelegate called blocks.
I would like to observe whenever an object is added to this array.
I've read other posts, but I can't understand why this isn't working.
In my app delegate class, I implement
- (void)insertObject:(id)obj inBlocksAtIndex:(NSInteger)index
{
[blocks insertObject:obj atIndex:index];
}
In my view controller's init method, I add an observer to my AppDelegate reference.
boardModel = [[UIApplication sharedApplication] delegate];
[boardModel addObserver:self forKeyPath:#"blocks" options:0 context:NULL];
In my view controller's viewDidLoad method, I try invoking the KVO Indexed array accessor I implemented previously,
[boardModel insertObject:[[Block alloc] init] inBlocksAtIndex:0];
Then I implement my observeValueForKeyPath method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"blocks"])
{
NSLog(#"ADDED");
}
}
I've tried adding an NSLog statement before the if statement in observeValueForKeyPath, and it seems as if it's never being called.
I've also tried NSLogging [[boardModel blocks] count], and it says the count is 1 (the object is being added).
I must be missing something.
The catch is that NSArrays don't respect KVO, so observing the key path count won't work.
If this is MacOSX, use NSArrayController. otherwise implement a wrapper class for the array that triggers the KVO calls when adding/removing contents of the array, and passes across all other calls.
You're observing the blocks property of the app delegate, not the blocks array itself. Hopefully the following example will make the difference clear:
// This will fire KVO as you're changing the app delegate's `blocks` property.
appDelegate.blocks = [NSMutableArray array];
// This will not fire KVO as the app delegate's `blocks` property still points
// to the same object; from the app delegate's perspective, nothing's happened.
[appDelegate.blocks addObject:#"Object"];
If you want to be notified when the contents of the blocks array changes, observe a property on the array itself—something like count. Updating your code:
[boardModel.blocks addObserver:self forKeyPath:#"count" options:0 context:NULL];
Did you try
- (void)insertObject:(id)obj inBlocksAtIndex:(NSInteger)index
{
[[self mutableArrayValueForKey:#"blocks"] insertObject:obj atIndex:index];
}
I just open-sourced a very small Objective-C library that adds a delegate to NSMutableArray. It might help you achieve what you were trying to do. Check out FCMutableArray on GitHub
It's not that NSMutableArray is not KVO-compliant in some respects, it's that all KVO-compliant properties must be NSObjects. "count" is an NSUInteger; you can't observe that property directly. If it was an NSNumber object, you could.
From the NSArray header file:
#interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>
#property (readonly) NSUInteger count;
Why was the answer accepted, by the way? It's clear the answerer did not test his answer.

Using one setter for all model iVars

I have a series of models for my application. Across all these models there are (will be) some 200 or 300 instance variables. The application stores its persistent data on a web-based server (MySQL - but I guess that part doesn't matter). Whenever a model iVar is updated I need to make a call to the server to update the appropriate value for that iVar.
My current model strategy is (header file):
#interface MyModel : NSObject {
NSString * firstName;
NSString * lastName;
}
#property (readwrite, copy) NSString * firstName;
#property (readwrite, copy) NSString * lastName;
#end
(implementation file):
#implementation MyModel
#synthesize firstName;
#synthesize lastName;
-(id)init {
[super init]
[self setFirstName:#"George"];
[self setLastName:#"Kastanza"];
return self;
}
-(void)setFirstName:(NSString *)aName {
// call method to update server with new value here
firstName = aName;
}
-(void)setLastName:(NSString *)aName {
// call method to update server with new value here
lastName = aName;
}
#end
The problem is that if I have 200 or 300 iVar's all needing to go through the same update call to the server that means writing a lot of setters. Moreover, if I need to make a change to the method call, I'd have to update each and every method in every setter i the entire application.
Is there a process by which I could run every set of an iVar through a method first, before setting?
I thought of having just a NSMutableDictionary per model object to store all of the iVar's, but that abstracts the setters and getters and may introduce a big memory footprint for so many dictionaries. However, doing it this way means that every time the dictionary is set I could pass it through one method.
As I understand it dynamically adding iVar's at runtime to an object model is considered a bad thing because of the pointer referencing for any subclasses that may be dependent upon the model (the subclass pointer doesn't get offset unless a complete recompile is done).
Any ideas and suggestions much appreciated.
Update
Based upon Ole's recommendation here is the solution (although it uses a little more code than a few lines unfortunately)...
In the model I added a method that I can set when I need to. I didn't call the method directly from the init, because adding a whole bunch of results returned from the server would trigger the observers for every object added. So I call the method after I have initialized and updated the first grab from the server.
Here's the code...
-(void)registerObservers {
[self addObserver:self
forKeyPath:#"firstName"
options:NSKeyValueObservingOptionNew
context:NULL];
[self addObserver:self
forKeyPath:#"lastName"
options:NSKeyValueObservingOptionNew
context:NULL];
}
Then I add the observer to the model:
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqual:#"firstName"]) {
// Do whatever I need to do
}
if ([keyPath isEqual:#"lastName"]) {
// Do whatever I need to do
}
}
In my real implementation I also happen to post a notification of the object set to self so that I can update anything that should be listening but isn't paying attention (like stuff in NSArrayControllers).
Use Key-Value Observing. You have to manually register yourself as an observer for every property, though.