Properly observing object using KVO - ios

I have a view which is observing the properties of a single object using KVO. I have observed all the properties of the object in the view.
[person addObserver:self forKeyPath:#"name" options:NSKeyValueObservingOptionNew context:NULL];
[person addObserver:self forKeyPath:#"photo" options:NSKeyValueObservingOptionNew context:NULL];
[person addObserver:self forKeyPath:#"address" options:NSKeyValueObservingOptionNew context:NULL];
Now, when there is change in just one property, it seems to be fine but when the whole object change, the notification gets triggered 3/4 times in just fraction of seconds. I need to load the data from the network based on the changes. While a single property change creates a single network request, but if multiple properties change at the same time. It creates a queue of request for the same object. This leads to some problem. How can I observe multiple properties at the same time and load only once even if all the properties changes.
Please do help me. This is a serious trouble, I have got into.

You can use a dispatch source in Grand Central Dispatch to coalesce the property change observations so they aren't happening more often than you can handle them.
#implementation Controller
{
dispatch_source_t source;
}
- (id)init
{
self = [super init];
if (self)
{
//We are using data add source type, but not actually using the added data.
source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(source, ^{
//Insert your network call to load data from the network.
//The event handler will only be called when another event handler is not being processed. So you won't attempt to do another network call until the last call was completed.
});
//Dispatch sources always start out suspended so you can add the event handler. You must resume them after creating them if you want events to be delivered)
dispatch_resume(source);
}
return self;
}
- (void)dealloc
{
dispatch_release(source);
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
//Tell dispatch source some data changed. It will only call the event handler if an event is not currently being handled.
//You could add a custom timer based coalesce here for when no events are currently being processed if you want to delay all initial events to potentially wait for more changes
dispatch_source_merge_data(source, 1);
}
#end
So the first property change notification triggers the dispatch source event handler. Subsequent property changes that happen while an existing event is running are queued to run as soon as the last one is complete. This means if 5 properties change in quick succession, you will get 2 network calls (instead of 5 network calls). You can add a custom timer based coalesce when no events are being processed if you would prefer to sacrifice instant responsiveness to notifications to eliminate the second network call.

Related

Do something on each property change

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.

Track NSManagedObject property when it was updated and immediately update view

I have a question how can I listen to changes in my data model.
I have NSManagedObject email with property progress. So while app is sending email, I every time update property progress.
I want to listen to data model and if changed update my view.
I added:
for (SAPEmail *email in _emails)
{
[self addObserver:email forKeyPath:#"progress" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"progress"])
{
SAPEmail *email = object;
NSLog(#">>>>>>>>>>> progress: %#", email.progress);
}
}
But seems it does not work for me.
I also using MagicalRecord.
I also tried to observe context
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(handleDataModelChange:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:myManagedObjectContext];
But when I debug my data model already was being update 10 times (because I update progress from 0 - 9), but handleDataModelChange invoked just once after all update where made. But I need to get all 10 update each time when data model updated to update progress view.
One more if I use this
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:nil];
and then for example try to refresh data:
- (void)managedObjectContextDidSave:(NSNotification *)notification
{
NSLog(#">>>>>>>>>+++++");
_emails = [SAPCoreDataEmailHelper emailsWithStatus:EmailStatusInProgress];
[_theTableView reloadData];
}
+ (NSArray *)emailsWithStatus:(EmailStatus)status
{
NSPredicate *prediacte = [NSPredicate predicateWithFormat:#"status == %d", status];
NSArray *emails = [SAPEmail MR_findAllWithPredicate:prediacte];
return emails;
}
I can see how works NSLog but then my app is freeze.
Even better, hold your fetched objects in a NSFetchedResultsController. In this way you can automatically monitor very specific entities without going into too much detail for observing every attribute.
Have you controller implement the NSFetchedResultsControllerDelegate methods. It is really simple, check out the Xcode templates using Core Data (e.g. Master-Detail Project, check "Core Data").
You're setting up the observer incorrectly, use the following code to set up your observations.
for (SAPEmail *email in _emails)
{
[email addObserver:self forKeyPath:#"progress" options:NSKeyValueObservingOptionNew context:NULL];
}
I also recommend setting the context parameter for KVO observations to be triggered properly.
Update
When you want to observe changes from Core Data, you have some options. I don't think you are grasping that there are multiple ways to do it. The first way is to watch for changes to a single property on a single object. This is the KVO approach. If you want to listen to save events for a particular context, then you'll need to use the NSNotificationCenter approach. You'll need to mix the two when you're making changes to multiple contexts. That is, if you're observing a property on an object in context A, and you make changes to that object/property in context B, you need to set up the NSNotificationCenter observation handler on NSManagedObjectContextDidSaveNotification to merge the new changes from context B into context A. From there, when the context merge is complete, then your KVO observation on property/object in context A is then triggered.
In your case, in your managedOjbectContextDidSave: method, you need to call
-mergeChangesFromContextDidSaveNotification: to merge your changes between contexts. I also recommend using multiple contexts, and not hold a single context in your SAPCoreDataEmailHelper class. When you get to multithreaded scenarios, you will encounter random crashes with a single context and multiple threads.

Smooth scrolling of text (Autoscroll) in UITextView iOS

I am working on a project where I there is some text in UItextview. The app wants to continuous smooth scroll that text and also wants to manage its scrolling speed. I mean here the text should scroll smoothly and the app contains slider where I can manage the speed.
Below is some sample code which I am using.
- (void) autoscrollTimerFired:(NSTimer *)timer {
[self.completeText setContentOffset:CGPointMake(0, self.completeText.contentOffset.y + 1.0) animated:NO];
if (self.completeText.contentOffset.y != self.completeText.contentSize.height - self.completeText.frame.size.height) {
scrollingTimer = [NSTimer scheduledTimerWithTimeInterval:velocityFactor target:self selector:#selector(autoscrollTimerFired:) userInfo:nil repeats:NO];
} else {
[scrollingTimer invalidate];
}
}
The Velocity factor is the number of seconds which ranges between 0.0 to 2.5.
It works nice in simulator but in device it moves with jerks or I must say like pausing at after some line.
Could you please suggest any solution here? All suggestions are welcome.
The NSTimers actually just periodically fire events into the enclosing NSRunLoop, which each thread has (or should have). So, if you have a child (or background) process running in a different thread, the NSTimers will fire against that thread's NSRunLoop instead of the application's main NSRunLoop.
NSTimer events fire on the thread where you scheduled the timer. If you scheduled the timer on the main run loop, then the timer will fire on the main thread and be “safe and synced” with input events.
So I have some suggestions for try that (I am not sure how much it will be successful in your case)
Don't use NSTimer. Try to call from main thread via selector with "afterDelay". As given in code
[self performSelector:#selector(autoScroll) withObject:nil afterDelay:1.0];
Use KVO or may be NSNotification for event triggering. (Not do it directly ,I Think better approach.)
I prefers KVO , so writing here the steps to use it :-
Make a class with a variable of NSNumber (which should accessible outside the class). Make a object of that class and add a Observer on it. (here model is object of that class).
[model addObserver:self forKeyPath:#"name" options:(NSKeyValueObservingOptionOld |
NSKeyValueObservingOptionNew) context:nil];
Implements it's delegate methods.
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
// Do here what ever you want to do. It will call every time whenever there will be any change in that class variable.
}
Make a method in controller "autoScroll" which called via afterDelay selector. And change the value of the NSNumber vairabe value (any logic incremental way. Doesn't affect a lot).
Hope this helps you !!! Try This ...best of luck

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;
}

Previously posted solution to getting Notification (on NSOperationQueue finishing all tasks) is not working for me

Get notification when NSOperationQueue finishes all tasks
I have the same issue as the one posted by #porneL in the post above. I tried the solution posted by #NickForge (one that received 57 votes), but I am obviously doing it wrong because it does not work for me. Here is the problem setup and my implementation:
I need to start a spinner before kicking off a set of web-service operations, and stop the spinner when they are complete. The webservices are invoked through a shared AFHTTPClient instance (part of the AFNetworking package) which adds them to its NSOperationQueue.
I set up an observer in the ViewController from which the data loads are kicked off. Did this using the answer from the above post. Implementation in my VC looks like:
In my ViewController's init method:
//add WelcomeVC as an observer for AFHTTPClient dataloadOps notifications
[[[MyCustomAFHTTPClient sharedClient] operationQueue] addObserver:self forKeyPath:#"DataLoaderEvent" options:0 context:NULL];
In my ViewController's observeValueForKeyPath method:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if( (object == [[MyCustomAFHTTPClient sharedClient] operationQueue])
&& [keyPath isEqualToString:#"DataLoaderEvent"]) {
if ( [[[MyCustomAFHTTPClient sharedClient] operationQueue] operationCount] == 0) {
NSLog(#"EUREKA!!! QUEUE IS EMPTY! DATALOAD HAS COMPLETED!");
}
}
}
The ViewController's observeValueForKeyPath method however never gets called!
Any help to get this working would be most appreciated so I can then complete implementing the spinner.
Does operationQueue have a property called DataLoaderEvent? Normally one monitors the "operationCount" property of an NSOperationQueue.
See http://developer.apple.com/library/ios/#documentation/Cocoa/Reference/NSOperationQueue_class/Reference/Reference.html#//apple_ref/doc/uid/TP40004592
"The NSOperationQueue class is key-value coding (KVC) and key-value observing (KVO) compliant. You can observe these properties as desired to control other parts of your application. The properties you can observe include the following:
operations - read-only property
operationCount - read-only property"
try this:
[operation setCompletionBlock: ^{
NSLog(#"Finished an image.");
}];

Resources