Is there a way to observe changes in derived properties? For example, I want to know when a CALayer has been added as a sublayer so that I can adjust its geometry relative to its (new) parent.
So, I have a subclassed CALayer, say CustomLayer, and I figured I could register an observer for the property in init:
[self addObserver:self forKeyPath:#"superlayer" options:0 context:nil]
and implement observeValueForKeyPath:ofObject:change:context. Nothing ever happens because, presumably, superlayer is a derived property (the attr dictionary stores an opaque ID for the parent). Similarly, I can't subclass setSuperlayer: because it is never called. In fact, as far as I can tell there are no instance methods called or public properties set on the sublayer when a parent does [self addSublayer:aCustomLayer].
Then I thought, OK, I'll subclass addSublayer like this:
- (void)addSublayer:(CALayer *)aLayer {
[aLayer willChangeValueForKey:#"superlayer"];
[super addSublayer:aLayer];
[aLayer didChangeValueForKey:#"superlayer"];
}
but still nothing! (Perhaps it's a clue that when I make a simple standalone test class and use the will[did]ChangeValueForKey: then it works.) This is maybe a more general Cocoa KVO question. What should I be doing? Thanks in advance!
Well, superlayer is defined as a readonly property, which means that there's no setSuperlayer: method. (If there is, it would be private, and you probably shouldn't use it.) If I had to make a guess, it would be that the superlayer property just isn't KVO-compliant. And, aside from that, I generally don't think it's a good idea for classes to observe themselves.
Maybe there's another way of doing this. When a layer is added to a superlayer, the onOrderIn action takes place. Now, actionForKey: is an instance method that gives a layer an opportunity to customize the default animations for certain properties. You could override actionForKey: to detect when the onOrderIn action takes place, do your thing, then call super's implementation.
I consider this a pretty messy hack, too, though. But it should be a bit more "self-contained" than having to use custom layers for everything and messing with KVO messages.
[self addObserver:self forKeyPath:#"superlayer" options:0 context:nil]
Don't observe yourself through KVO. Change your accessors instead.
Similarly, I can't subclass setSuperlayer: because it is never called.
I take it you tried this and added NSLog and found that it wasn't called?
Then I thought, OK, I'll subclass addSublayer like this:
- (void)addSublayer:(CALayer *)aLayer {
[aLayer willChangeValueForKey:#"superlayer"];
[super addSublayer:aLayer];
[aLayer didChangeValueForKey:#"superlayer"];
}
And the parent layer is also a CustomLayer, right? If the parent layer is a plain CALayer, anything you do in CustomLayer will have no effect.
Related
I was looking into an open source pull-to-refresh control and it swizzle lifecycle methods on a UIViewController category like so:
- (void)INBPullToRefreshView_viewWillAppear:(BOOL)animated
{
[self setClearNavigationBar:YES];
[self INBPullToRefreshView_viewWillAppear:animated];
UITableView *tableView = self.pullToRefresh.tableView;
tableView.contentOffset = tableView.contentOffset;
self.pullToRefresh.showPullToRefresh = YES;
}
I get that when viewWillAppear was called it mapped to the above method, and that calling [self INBPullToRefreshView_viewWillAppear:animated]; will map to the original viewWillAppear.
However, what does the following do?:
tableView.contentOffset = tableView.contentOffset;
Here's the github source for the control.
I would suspect the author is trying to use a side-effect of setContentOffset:, perhaps forcing a recalculation. But the author seems active on the project, so why not ask intmain in a github issue?
Of course the standard warnings that this kind of method swizzling is extremely dangerous and fragile apply.
I believe you're asking something unrelated to the swizzling itself?
Setting the contentOffset property will cause a scrollViewDidScroll: message sent to the delegate of your object. There's probably a cleaner way to accomplish that (or at least it should have a comment)
My NSDocument subclass implements selectAll:. Only problem is, I'm using NSTableView, and it also implements selectAll:. However, the selectAll: action in NSTableView doesn't do what I want, and it does prevent the selectAll: method in my Document class from ever being reached in the responder chain.
I already have a subclass of NSTableView, and after poking around a bit I got things working the way I want by adding a respondsToSelector: method to my NSTableView subclass which lies to the runtime by telling it there is no selectAll: action:
-(BOOL)respondsToSelector:(SEL)targetSelector
{
if (targetSelector == #selector(selectAll:)) {
return FALSE; // we don't want tableView's implementation of selectAll
}
return [super respondsToSelector:targetSelector];
}
This seems to work fine, allowing the selectAll: method in my document subclass to do its thing. But this solution leaves me a bit uneasy. What about other action methods I have implemented in this subclass? Do I need to manually check and return true for each of them? I do have two actions defined in this subclass, moveLeft: and moveRight:, and they seem to work, even though I am not handling them in respondsToSelector:. So my question is, am I doing this correctly, or is there something I am missing? Or perhaps there is some entirely different way to do this properly?
By the way, I got the idea of overriding respondsToSelector from this post on the OmniGroup forum:
http://mac-os-x.10953.n7.nabble.com/Removing-an-action-from-a-subclass-td27045.html
Sending a message to super affects which implementation of that method we use. It doesn't change who self is.
So let's try to imagine how respondsToSelector: works. Given a selector mySelector, it probably introspects every class up the superclass chain, starting with [self class], to see whether it actually implements mySelector.
Now then, let's say your subclass is called MyTableView. When MyTableView says
[super respondsToSelector:targetSelector]
what happens? The runtime will look up the superclass chain for another implementation of respondsToSelector:, and eventually will find NSObject's original implementation. What does that implementation do? Well, we just answered that: it starts the search for an implementation of targetSelector in [self class]. That's still the MyTableView class! So if you have defined moveLeft: in MyTableView, respondsToSelector: will find it and will return YES for moveLeft:, exactly as you hope and expect.
Thus, to generalize, the only selector for which this search has been perverted is the search for selectAll: - exactly as you hope and expect. So I think you can relax and believe that what you're doing is not only acceptable and workable but the normal solution to the problem you originally posed.
You might also like to look at the Message Forwarding chapter of Apple's Objective-C Runtime Programming Guide.
When I try setting up observation of this specific key, nothing happens. Here is what I mean.
I have a standard Cocoa application, with an NSTableView, and I figured out how to change the image shown in an image view based on what cell was selected.
Now, I am trying to figure out how to disable/enable buttons by the selected index. What I mean by this, is that I have a button in the window, that is disabled on certain indexes.
SomeClass's init method
-(id)init {
if (self=[super init]) {
[arrayController addObserver:self forKeyPath:#"selectionIndex" options:NSKeyValueObservingOptionNew context:NULL];
}
}
However, when I implement the observeValueForKeyPath: method, the changes are not being picked up as I pick new indexes. As a matter of fact, the outlet shows (null) if I try logging it. However, when I add the observer in AppDelegate, AppDelegate (when specified as the observer) picks up changes.
Is there some reason my generic SomeClass object does not? Should it be done a different way?
NOTE:
I tried subclassing SomeClass as a NSWindow, then making the window's owner SomeClass, and setting up the observer in awakeFromNib, and this works, but seems like a bad way to do it.
The init method happens too early in the process, before the outlets are connected (I think). Putting the code in awakeFromNib will work correctly. This is from Apple's docs in the NSObject class reference:
The nib-loading infrastructure sends an awakeFromNib
message to each object recreated from a nib archive, but only after
all the objects in the archive have been loaded and initialized. When
an object receives an awakeFromNib message, it is guaranteed to have
all its outlet and action connections already established.
I have an entity with several properties, one of them called lastModificationDate. Whenever any of the object's properties is set, I'd like to update the lastModificationDate.
If I were not using Core Data, I would just provide my own setter for the properties and update lastModificationDate. However, I'm not sure if I should mess around with CoreData's properties.
What's the best way to do this?
Overriding the setters can easily be done, you have to make sure you fire the right notifications for everything else to work (including KVO).
- (void) setThing:(NSObject *)myThing {
self.lastUpdateDate = [NSDate date];
[self willChangeValueForKey:#"thing"];
[self setPrimitiveThing:myThing];
[self didChangeValueForKey:#"thing"];
}
This being said, if all you need to do is the code I showed (essentially setting the value and updating the last update date), you are much better off using Key-Value Observing and reacting to the notifications. It's easier and cleaner.
You shouldn't override property mutators (setters) if you're working with an NSManagedObject subclass because those implementations are provided at runtime (hence #dynamic instead of #synthesize). You could if you really wanted to, but it's messier and there's no reason to. Use Key Value Observing (KVO) instead. It'll let you know when a value is changed.
Apple's KVO documentation is great: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html#//apple_ref/doc/uid/10000177i
While working on a small project I found myself needing to do some custom drawing via drawRect: in one of my UIView subclasses. I noticed when I overrode drawRect: that the default background color of the UIView subclass had changed from transparent to black (by default background color I mean the color the view draws itself when its backgroundColor property is nil.) Even with an empty drawRect: or a drawRect: that simply calls [super drawRect:] I noticed this behavior.
This isn't really a problem, as simply setting a backgroundColor to a non-nil value works regardless of whether drawRect: is overridden. However, it did make me start thinking about how UIView knows whether drawRect: is overridden by a subclass. I know Objective-C offers facilities to determine if a class or even its superclass responds to a certain selector. But how could a superclass possibly know if its subclass has overridden one of its methods? And, if this type of introspection is indeed impossible, what could be going on in my example?
That is pretty bizarre (but sounds like it is intended). Simply adding:
- (void) drawRect:...
{
[super drawRect:...];
}
Triggers the behavior? Atypical. In any case, it is trivial to use the Objective-C runtime API to introspect class implementation details quite thoroughly. See the Objective-C runtime reference.
UIView's documentation for -drawRect: goes into considerable detail about subclassing. It quite specifically states that you don't need to call super when directly subclassing UIView, indicating that the class is likely optimized to doing the minimal amount of extra work (like drawing a background that is obliterated entirely in your implementation).