Observe change in a property inside property - ios

I was wandering if it was possible to use didSet for a property inside a property.
Let's say I have a function resize() that I want to call every time the text property of a UILabel is set to a new value. I tried using the label's own didSet, but it didn't work, as I was expecting. Is there a way to do this?

it didn't work, as I was expecting
What do you mean by that? didSet works properly for overridden properties. Try this:
class MyLabel : UILabel {
override var text : String? {
didSet {
resize()
}
}
private func resize() {
print("text is now \(text). resizing...")
frame = CGRectMake(...
}
}

Properties modification can be tracked using KVO. After your view, which has reference on target label will be initialised, you need to add add observer like this:
[self.label addObserver:self forKeyPath:#"text"
options:NSKeyValueObservingOptionNew context:nil];
In your view (same from which you called method above) you should have handler method like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
// If you subscribed only for one value, you can resize from here or
// add conditions.
}
When you will leave view, don't forget to unsubscribe like this:
[self.label removeObserver:self forKeyPath:#"text" context:nil];
Warning: addObserver and removeObserver should be balanced. If you will call removeObserver extra time, your application will crash.

Related

KVO check for change of clipsToBounds of all subviews in an UIView in objective c

I am trying to implement a KVO example for clipsToBounds property of all subviews in my UIView. I do not quite understand how to change the value in observeValueForKeyPath method. I am using this code:
-(void)ViewDidLoad{
[self.navigationController.view addObserver:self forKeyPath:#"clipsToBounds" options:NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(#"Triggered...")
}
It is triggered when ever i change the property clipToBounds of a subview that exists in the UIView i have. I need to change the value back to false for every trigger that happens. What should i write inside the observeValueForKeyPath to change the clipsToBounds property? Any help appreciated.
of course adding the Observer must be done before it works.
Guessing your typo in "ViewDidLoad" would just never be called because it should be "viewDidLoad".
Apart from that your KVO pattern could look like..
static void *kvoHelperClipsToBounds = &kvoHelperClipsToBounds;
-(void)viewDidLoad {
[self.navigationController.view addObserver:self forKeyPath:#"clipsToBounds" options:NSKeyValueObservingOptionNew context:&kvoHelperClipsToBounds];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == kvoHelperClipsToBounds) {
NSLog(#"context compared successful...");
//be careful what you cast to.. i dont check isKindOf here.
UINavigationBar* navbar = (UINavigationBar*)object;
if (navbar.subviews.count > 1) {
__kindof UIView *sub = navbar.subviews[1];
if (sub.clipsToBounds) {
dispatch_async(dispatch_get_main_queue(),^{
sub.clipsToBounds = NO;
[self.navigationItem.titleView layoutIfNeeded];
});
}
}
}
// or compare against the keyPath
else if ([keyPath isEqualToString:#"clipsToBounds"]) {
NSLog(#"classic Key compare Triggered...");
}
else
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
[super observeValueForKeyPath...] passes not recognized keyPath to super to let super's class KVO work, otherwise those would be ignored if super's implementation would rely on observing them. Which should also explain how you could observe all subviews if needed. But think about that there could be potentially hundrets of subviews triggering observeValueForKeyPath if a subclass of UIView would implement it and all subviews (or the ones you like) would be inherited also from this special subclass.
When you change clipsToBounds inside the KVO where you observe it, you possibly invoke a loop, specially when you watch both - old and new values. you would change the property, the property triggers kvo, kvo changes the property, the property triggers kvo and on and on.
set [self.navigationController.view setClipsToBounds:YES] to change the property. But if done inside KVO it will trigger KVO again as explained.
Usually you would set clipsToBounds in -initWithFrame: or in -initWithCoder: or via Interface Builder and maybe just observe if it gets changed to adapt some other code.
Sidenote: the context just needs to be unique to distinguish it from other KVO.. it could also be reference to a real objects pointer.
Don't forget added Observers must be removed before deallocation.

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.

Listen UITextField setText

I have a UITextField which is updated from a different module (I'm passing my UITextField to the above mentioned module to fill it). I need a way to identify when text value of the UITextField is changed inside the module which the UITextField originally is. UITextField Delegate methods or UIControlEvents will not work as the text is fill programmatically.
Put the following somewhere, like -viewDidLoad:
[textField addObserver:self forKeyPath:#"text" options:NSKeyValueObservingOptionNew context:nil];
And then add a -observeValueForKeyPath like:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == textField)
{
// Do whatever with your text field here
}
}
This will only be called when the text is set programmatically though, not when the user is typing in the field.
Or just subclass UITextField and pass your subclass in. You can override setText: as needed
I don't quite understand what you mean by a "different module", but you can try subscribing to UITextFieldTextDidChangeNotification.
Another way to get notified is by using Key-Value Observing. I don't know if it works for the text property, though.

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.

Is it possible to a property in a container view before awakeFromNib is called?

I have a container view that holds a view controller. I need to set a non-UI property in this view controller before awakeFromNib is called. However, the prepareForSegue method for the embed segue isn't called until after awakeFromNib happens.
Is there any way to pass this information to the contained view controller before awakeFromNib?
I have a similar issue in one of my apps.
Basically, I have a ViewController that has a property for the data model, but I am never sure when in my lifecycle the data model is actually set. My solution was to use Key-Value Observing to receive a callback when it's set.
Somewhere before the value can be set:
[self addObserver:self forKeyPath:#"propertyName" options: 0 context: nil];
Callback:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"propertyName"]) {
//do something
}
}
remember to unregister (I do it in my dealloc)
[self removeObserver:self forKeyPath:#"propertyName"];

Resources