Reputation: 521
I'm building a custom view that contains several other subviews (NSTextField
, WebView
,...). I'd like to have my custom view draw a different border when one of the subviews is the first responder, and act as a single item that can be acted upon with menu items and keyboard shortcuts. It looks something like this:
+-------------+
| NSTextField |
+-------------+
| WebView |
+-------------+
So far, I've had success subclassing NSTextField
and others to notify a delegate when - (BOOL)becomeFirstResponder
and - (BOOL)resignFirstResponder
are called. This approach doesn't work with WebView
though, as it itself contains many subviews--I can't subclass them all!
Is there a better way to detect when subviews change their first responder status? Or a better way to create a custom view?
Upvotes: 10
Views: 4743
Reputation: 26187
I had this problem with iOS / UIWebView
, which doesn't implement makeFirstResponder
in UIWindow
, nor webViewDidEndEditing
or shouldBeginEditingInDOMRange
. However, with the use of Swizzling I was able to create a helper category that allows for retrieval of the current first responder, as well as posting a notification every time first responder changes. Really frustrating how all this should be public API, but isn't, as swizzle isn't normally a first goto, but this worked well enough.
First, setup your category header:
@interface UIResponder (Swizzle)
+ (UIResponder *)currentFirstResponder;
- (BOOL)customBecomeFirstResponder;
@end
Then Category implementation
@implementation UIResponder (Swizzle)
// It's insanity that there is no better way to get a notification when the first responder changes, but there it is.
static UIResponder *sCurrentFirstResponder;
+ (UIResponder *)currentFirstResponder {
return sCurrentFirstResponder;
}
- (BOOL)customBecomeFirstResponder {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithCapacity:2];
if(sCurrentFirstResponder) {
[userInfo setObject:sCurrentFirstResponder forKey:NSKeyValueChangeOldKey];
}
sCurrentFirstResponder = self;
if(sCurrentFirstResponder) {
[userInfo setObject:sCurrentFirstResponder forKey:NSKeyValueChangeNewKey];
}
[[NSNotificationCenter defaultCenter] postNotificationName:kFirstResponderDidChangeNotification
object:nil
userInfo:userInfo];
return [self customBecomeFirstResponder];
}
@end
Finally, using a helper like JR Swizzle, swap the classes.
#import "JRSwizzle.h"
- (void)applicationLoaded {
if(![UIResponder jr_swizzleMethod:@selector(becomeFirstResponder) withMethod:@selector(customBecomeFirstResponder) error:&error]) {
NSLog(@"Error swizzling - %@",error);
}
}
Thought I'd share. Valid in App store as it doesn't utilize private API, and while Apple cautions against swizzling base classes there is no edict against doing so.
Upvotes: 1
Reputation: 9505
Both WebViewEditingDelegate method will be call,
Resign first responder:
-(void)webViewDidEndEditing:(NSNotification *)notification
{
}
and when become first responder:
-(BOOL)webView:(WebView *)webView shouldBeginEditingInDOMRange:(DOMRange *)range
{
return YES;
}
Upvotes: 1
Reputation: 521
A different approach would be to override the -makeFirstResponder:
method on NSWindow
to send out a notification.
- (BOOL)makeFirstResponder:(NSResponder *)responder {
id previous = self.firstResponder ?: [NSNull null];
id next = responder ?: [NSNull null];
NSDictionary *userInfo = @{
BrFirstResponderPreviousKey: previous,
BrFirstResponderNextKey: next,
};
[[NSNotificationCenter defaultCenter] postNotificationName:BrFirstResponderWillChangeNotification object:self userInfo:userInfo];
return [super makeFirstResponder:responder];
}
You can then listen for the notification in your custom view or a view controller and check if the previous or next responders are subviews using -isDescendantOf:
and set needsDisplay
as needed.
This is not an ideal solution though, because the custom view is no longer self-contained. It works for now, but hopefully a better approach will be shared.
Upvotes: 5