Do something on each property change - ios

I have several properties in my class, I would like to call saveToFile on each property change.
I prefer not to override the setter of each property. Should I override
-[NSObject methodForSelector]? What is the best way to go?

You can register as observer to the properties you want monitored. Cocoa's KVO functionality will help you here.
Basically you need to call addObserver:forKeyPath:options:context: and register to be notified when the properties change. When this happens, the runtime calls the observeValueForKeyPath:ofObject:change:context: method on the object registered as observer. You can do here the saving you want to do.
Example for registering:
for(NSString *propName in self.propsIWantMonitored) {
[self addObserver:self forKeyPath:propName change:0 context:#selector(saveToFile)];
}
and for dealing with the change of the prop values:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
// make sure we don't interfere with other observed props
// and check the context param
if (context == #selector(saveToFile)) {
[self saveToFile];
}
}
and for de-registering:
for(NSString *propName in self.propsIWantMonitored) {
[self removeObserver:self forKeyPath:propName context:#selector(saveToFile)];
}
The code samples above assume you have declared an array of properties to monitor, that you use to register as observer to. You use the context parameter to determine if observeValueForKeyPath was called as a response to the observer you just registered, or not, in order not to get into conflict with other KVO registrations made from other parts of your class.
Alternative (and more energy efficient) approach to your problem
There's one caveat with the above approach: if multiple properties are set consecutively, then the saveToFile method will be called multiple times in a short period of time, which might cause performance bottlenecks and increase the energy usage of your application.
An alternative approach would be to have a dirty flag that gets set in observeValueForKeyPath: and gets reset in saveToFile. And you can have saveToFile check the flag and don't go use the file system if the object is not dirty.
You can schedule a timer that will periodically call saveToFile, this way multiple properties set at once will result in only one disk access. You can always manually call saveToFile when you feel want an immediate save.
Note. By timer I was referring to a GCD timer, as NSTimer also has a negative energy impact on your application.

What you want is called Key-Value-Observing or KVO.
You register for example a method that gets called every time the property changes.
If you have a text field and you want to listen to changes to its text, you would register like this
[self.textField addObserver:self forKeyPath:#"text" options:NSKeyValueObservingOptionNew context:nil];
And in your class you would implement this method:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if ([keyPath isEqualToString:#"text"]) {
NSLog(#"Textfield changed - MAKE CHANGES HERE");
}
}
Here's a nice tutorial, if you aren't familiar with KVO:
http://www.appcoda.com/understanding-key-value-observing-coding/

Read up on Property Observers. An example in Swift:
var currentSession: Session? {
didSet {
if let session = self.currentSession {
// Write session to file.
}
}
}
For Objective-C, key-value observing might be more proper.

Related

Get all views of an app- every moment?

I need to find a way to create some class that is always alive in the app. This class doesn't know anything about the other classes in the project. It will be able to "follow" all UIViews on screen-so every moment I can check(loop) over the views and get a pointer to each of them.
It has to run live, and always check positions of views (is it a memory problem?)
Why a pointer? because I need to know everything about it, so if its some kind of moving animation, or maybe it has some meta data like tags, etc. so only knowing there is some view at a certain position is not enough.
Is it possible in iOS ?
The whole idea sounds like an antipattern. However, …
Simply traverse the view tree and add a KVO handler to every view for every interesting property.
- (void)traverseSubviewsOfParentView:(UIView*)view
for( UIView* subview in view.subviews )
{
[view addObserver:self forKeyPath:#"frame" options: NSKeyValueObservingOptionNew];
…
[self traverseSubviewsOfParentView:subview context:NULL];
}
Then implement the observation method:
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if( [#"frame" isEqualToString:keyPath] )
{
// Do what you want to do
}
…
}
Additionally you have to observe the subviews property of every view to get notified, when a view is inserted or removed.

ViewController observing an object property not working

I'm starting to learn objective-c and my first app would be simple. I created a class to handle a timer. It has a property currentTime, a startAndPause method and a stopTimer method.
I'm initialising my Timer in the viewDidLoad method of my ViewController as :
_minu = [[KUUMinuteur alloc] initWithDuration:#70];
Then, I want my ViewController to observe my _minu.currentTime property changes. So I did this :
[_minu addObserver:self forKeyPath:#"currentTime" options:NSKeyValueObservingOptionNew context:NULL];
And in my viewController, I wrote this method but it never triggers :
-(void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(#"Changing !!");
}
I don't know what am I doing wrong. :(
(My App is a single View app.)
EDIT : forgot to translate tempsPublic to currentTime
You probably are never changing the currentTime property of KUUMinuteur.
I wonder if you are trying to directly update _currentTime ivar that backs your currentTime property. When changing the value of a property you do not want to use the ivar (other than the init, dealloc, and custom setter methods, if any):
_currentTime = ...; // wrong
You want to use the setter, e.g.
self.currentTime = ...; // right
or
[self setCurrentTime:...]; // right
Make sure you use the setter or else the key-value notification will not take place.
See the Use Accessor Methods to Set Property Values section of the Advanced Memory Management Programming Guide. Or see the Automatic Change Notification section of the Key-Value Observing Programming Guide.
[_minu addObserver:self forKeyPath:#"currentTime" options:NSKeyValueObservingOptionNew context:NULL];
It should be currentTime instead of tempsPublic.

How can I tag a property in iOS and track it's state throughout the app?

Is there a way to tag a property or Instance variable throughout the life of the application and see it changes?
The idea would be to see how the property is changed and what class/object changes it's value when that value is passed around a lot? This way we could see its value every time it changes instead of filling the code with breakpoints.
You can not tag the property or instance variable.
But you can make the Key-Value Observing(KVO) for properties/instance variables to find the changes of old and new values. KVO allows you to observe changes to a property or value.
For example, If you set the observer on particular property/instance variable like this,
[self addObserver:self forKeyPath:#"object.sampleInstance" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
whenever you made the changes on that property like this
[object setSampleInstance:#"Hai"];
It will call
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
id newVal = (id)[change objectForKey:NSKeyValueChangeNewKey];
id oldVal = (id)[change objectForKey:NSKeyValueChangeOldKey];
NSLog(#"KVO new %#", newVal);
NSLog(#"KVO old %#", oldVal);
}
From this you can trace the property/instance variables

After unregistering, I get "class deallocated while key value observers were still registered with it"

I have some code which applies to a number of objects, registering my class as the KVO:
for (SPPanelManager *manager in self.panelManagers) {
[manager addObserver:self forKeyPath:#"dataFetchComplete" options:0 context:NULL];
[manager fetchData];
}
Then when it observes a change, which happens on every of these objects, I un-register:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"dataFetchComplete"] && ((SPPanelManager *)object).dataFetchComplete) {
[object removeObserver:self forKeyPath:#"dataFetchComplete"];
//Other stuff
}
}
Then when I leave the UIViewController later, I get these errors for each of the manager objects:
An instance of class 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.
I'm not sure why it's giving me this error - these are the only 2 places that KVO is ever referenced so it's not another observer.
Your class(observer) is being deallocated during some activity. You must unregister it before it is deallocated or not is in further use. Use code below in viewDidUnload: or dealloc:
for (SPPanelManager *manager in self.panelManagers) {
[manager removeObserver:self forKeyPath:#"dataFetchComplete" context:NULL];
}
Don't try to add or remove observers in observeValueForKeyPath:ofObject:change:context:. KVO expects the list of observers for a given Tuple(object, keyPath, context) to remain the same across a notification for that combination. Even it it works "sometimes" the behavior is non-deterministic because the order in which observers are notified is not guaranteed (it probably uses a set-type data structure internally.)
The simplest way around this problem might look something like:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"dataFetchComplete"] && ((SPPanelManager *)object).dataFetchComplete) {
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{
[object removeObserver:self forKeyPath:#"dataFetchComplete"];
});
}
}
This will cause the observation to be removed at the next possible point in the run loop (barring custom run loop modes, which might delay its execution a bit, but usually won't be a problem). Also, you shouldn't need to worry about either self or object getting deallocated because they will be retained by the block closure until the block has executed and is, itself, releases.
As you've discovered, KVO isn't a particularly great API for one-shot notifications.
As to the initial error message, you'll need to remove the observation. You can probably get away with doing this in the observing object's dealloc but you should really avoid doing "real work" in dealloc and removing observations is arguably "real work." Unfortunately, there's not a standard teardown pattern in Cocoa, so you'd have to trigger the teardown yourself, perhaps when you segue out of the view controller, or something like that. Another answer suggested viewDidUnload but that is deprecated in iOS 6 and will never be called, so it's not a good approach any more.
You've to simply dealloc your GMS_MapView Object and as well as remove the MapView Observer forkeypath.
(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[objGMS_MapView removeObserver:self forKeyPath:#"myLocation"];
//=> Set map delegate to nil (to avoid: mapView:regionDidChangeAnimated:]: message sent to deallocated instance )
objGMS_MapView.delegate = nil;
objGMS_MapView = nil;
}

KVO Loading a new view with existing model data

I've recently begun to discover what can be done with KVO and I'm refactoring some of my code and saving a lot of lines at the same time. I do face one issue that is so general that it makes me wonder whether a certain pattern is recommended.
In some cases I load a new view controller that needs to represent data from an already initialized model. On -viewDidLoad I would register for KVO:
[_model addObserver:self
forKeyPath:kSomeKey
options:NSKeyValueObservingOptionNew
context:(__bridge void *)(_model)];
and change my interface when values change:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqual:kSomeKey] && context == (__bridge void *)(_model)) {
[self updateSomeInterfaceElement];
}
Unfortunately and understandably, the view is not updated with current values from the model when I load my new view.
Is my best option to call -updateSomeInterfaceElement in -viewDidLoad? It doesn't seem to be a big deal like this, but when listening for 10-20 properties, it looks very inefficient (especially since all my -updateSomeInterfaceElement methods are mostly 1 line only, so no need to make them into a separate method). Is there any way to circumvent this, or is there a more elegant solution?
You want to change your options to include NSKeyValueObservingOptionInitial. This will cause KVO to fire a notification when you add the observer, providing the observer with the "initial" value.
Also, as an aside, you should get in the habit of calling super if observeValueForKeyPath:... is called for a notification you didn't sign up for. Also, it's a bit more bulletproof to avoid using "live" pointers in the role of KVO contexts (since a future object could have the same pointer if the current object is deallocated.) I generally prefer to use a pattern like this:
static void * const MyObservationContext = (void*)&MyObservationContext;
- (void)viewDidLoad
{
// ... other stuff ...
[self addObserver:self forKeyPath:#"model.someKey" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:MyObservationContext];
// ... other stuff ...
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == MyObservationContext)
{
// Do stuff
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

Resources