IOS重载私有类中的方法
来源:互联网 发布:歌曲下载软件推荐 编辑:程序博客网 时间:2024/04/28 08:34
Detecting backspace in a UITextField
I recently had need for a token field for iOS while implementing a "To:" field like the one in Mail.app. I ran into a problem, though. The token field consists of a label, some number of tokens, and a borderless UITextField at the end where the user can enter text for the next token. It looks something like this, if you'll forgive the ASCII art:
In Mail.app, if you're at the far left of the text field and hit the backspace key, the rightmost token will be highlighted. Another backspace deletes the token and puts focus back in the text field. But there's a problem.
You'd think this would be simple, but it's not. Some of my early attempts included:
DTrace, of course.
Using a boilerplate app containing a single UITextField, I pulled out Instruments and added a Trace instrument for
That is, "Trace every ObjC call to any object where the selector starts with the word 'delete'." I launched the app and waited for things to quiet down, then tapped in the text field and hit the backspace key on the keyboard. Immediately, a few delete-related methods showed up, including this stack trace:
Why hello there,
I love DTrace.
Okay, so we know there's something called a
If we class-dump
So it appears that the
Class-dump also tells us that
So rather than subclassing
We've got two problems down the "Subclass UIFieldEditor" path, though:
Dynamically creating classes at runtime isn't something you're likely to do very often, but it's fairly well documented, and there have been somegreat posts about it in the community. I won't bother going into more detail here—it's enough to know that the dynamically-created class works just like any other, once you get it all set up. You just need to allocate a new class pair, register the new class, and add the methods we want to override to the new class. (One caveat of note: calls to
Once we've created our
Let's go back to class-dump for a moment. Looking through the method list on
We could probably do it with
Key-Value Coding to the rescue! Key-Value Coding is built around the idea that if you know the name of a property, Cocoa can figure out what the appropriate getters and setters should be. So, rather than trying to figure out the exact method we want to call, let's just ask Cocoa to get the fieldEditor for us:
It's as easy as that.
At this point, we have a reference to the field editor, and we've created a dynamic
Our needs are actually quite simple; when
Here's my implementation:
It's really quite simple. We get a reference to the text field using ObjC associated objects, and call
I'm a little nervous about unilaterally changing the behavior of
With this implementation, the shared
This is about the point where everyone working on
In the meantime, use with caution. I haven't tried it in the App Store, but I suspect it will pass validation as there are no symbols referencing any private API in this implementation.
The code is on GitHub.
-------------------------------------------------| To: (token1) (token2) (token3) _____________ |------------------------------------------------- ^ ^ ^ ^ ^UILabel tokens UITextField
In Mail.app, if you're at the far left of the text field and hit the backspace key, the rightmost token will be highlighted. Another backspace deletes the token and puts focus back in the text field. But there's a problem.
UITextField
provides no way to detect a backward delete at the beginning of the field.Part 1: Discovery
You'd think this would be simple, but it's not. Some of my early attempts included:
- Implementing
-textField:shouldChangeCharactersInRange:replacementString:
in the text field's delegate and looking for an attempt change the range at (0,0). No dice—it's not called if the text isn't actually going to change. - Subclassing
UITextField
and overriding the-deleteBackward
method (part of theUIKeyInput
protocol, whichUITextField
conforms to viaUITextInput
).-[UITextField deleteBackward]
is never called when the backspace key is pressed. - Subclassing
UITextField
and overriding various methods declared in theUITextInput
protocol, such as-replaceRange:withText:
,-setSelectedTextRange:
, etc. Surprisingly, none of these are called either—despite declaring conformance to theUITextInput
protocol, it seems that UIKit doesn't actually use any of those methods when handling keyboard input.
UITextField
that claims to implement all these text-handling methods, and yet it doesn't seem to use them when handling keyboard input. It's probably forwarding them on to some other object, but how could we find out who that other object is?DTrace, of course.
Using a boilerplate app containing a single UITextField, I pulled out Instruments and added a Trace instrument for
-[* delete*]
.That is, "Trace every ObjC call to any object where the selector starts with the word 'delete'." I launched the app and waited for things to quiet down, then tapped in the text field and hit the backspace key on the keyboard. Immediately, a few delete-related methods showed up, including this stack trace:
Why hello there,
UIFieldEditor
with a UITextInputAdditions
category. We've been looking for you.I love DTrace.
Part 2: Digging through the dump
Okay, so we know there's something called a
UIFieldEditor
, and sinceUITextField
wasn't involved at all in the above stack trace, we can guess that the field editor may be doing all of the heavy lifting. So let's look atUIFieldEditor
a bit deeper.If we class-dump
UIFieldEditor
in UIKit, we see that the first three methods in its method list are these:+ (void)releaseSharedInstance;+ (id)sharedFieldEditor;+ (id)activeFieldEditor;
So it appears that the
UIFieldEditor
receiving keyboard input is likely a shared object that is reused whenever the first responder needs keyboard input. Since there can only be one first responder at a time, there's probably no need for more than one UIFieldEditor
.Class-dump also tells us that
UIFieldEditor
is a subclass of UIWebDocumentView
. This is interesting, because a bit of time in DTrace will also confirm that-[UITextField deleteBackward]
calls down to [UIWebDocumentView deleteBackward]
. It looks like the field editor is a view that gets overlaid on top of text input views, and probably handles most of the text editing experience.So rather than subclassing
UITextField
, it looks like we really want to be subclassingUIFieldEditor
if we want to do something special with -deleteBackward
.Part 3: Dark Runtime Magic
We've got two problems down the "Subclass UIFieldEditor" path, though:
UIFieldEditor
is a private class in UIKit, so we don't have its headers. Without the headers,@interface MyFieldEditor : UIFieldEditor
is going to cause a compiler error, since it won't know how to inherit fromUIFieldEditor
.- We don't control the
UIFieldEditor
instance used byUIKit
. Even if wecould create our ownMyFieldEditor
, we still don't know how to swap out the existing shared object for our own.
UIFieldEditor
at runtime and change the class of the existing field editor to be out new dynamic subclass. This sounds crazy—and it is. But it works1, and it allows us to add our own -deleteBackward
functionality without having to swap out existing objects and somehow inform UIKit of what we're doing.Dynamically creating classes at runtime isn't something you're likely to do very often, but it's fairly well documented, and there have been somegreat posts about it in the community. I won't bother going into more detail here—it's enough to know that the dynamically-created class works just like any other, once you get it all set up. You just need to allocate a new class pair, register the new class, and add the methods we want to override to the new class. (One caveat of note: calls to
objc_allocateClassPair()
and objc_registerClass()
fail under ARC, so if you're using ARC you'll have to do those in a file that's not using ARC.) You can see how this works in my sample implementation, linked at the end of this post.Once we've created our
MyFieldEditor
class, though, we still have to actually change the class of the existingUIFieldEditor
. This requires using the runtime function object_setClass(id object, Class newClass)
. ThenewClass
parameter is easy enough, but what are we going to pass it for theobject
? We know there's a UIFieldEditor
out there, but we still don't have a reference to it.Let's go back to class-dump for a moment. Looking through the method list on
UITextField
, you'll see a -(id)_fieldEditor
method. Sounds like exactly what we want. Unfortunately, we can't just toss that method declaration in a category and then call it directly; that's sure to fail App Store validation for using private API. So we need some way of calling that method without making it look like we're using that method.We could probably do it with
-(id)performSelector:
, but we clearly can't just create the selector with@selector(_fieldEditor)
; that will fail App Store validation just as quickly as calling it directly. We could construct it dynamically from a string, but ARC introduces some caveats when calling-performSelector:
with a dynamically-constructed selector because it can't guarantee to get the memory management right. It would be nice to have something that would work correctly without a lot of overhead.Key-Value Coding to the rescue! Key-Value Coding is built around the idea that if you know the name of a property, Cocoa can figure out what the appropriate getters and setters should be. So, rather than trying to figure out the exact method we want to call, let's just ask Cocoa to get the fieldEditor for us:
id fieldEditor = [someTextField valueForKey:@"fieldEditor"];
It's as easy as that.
Part 4: Making the call
At this point, we have a reference to the field editor, and we've created a dynamic
UIFieldEditor
subclass that we can use to customize its behavior. We never actually added any methods, though;MyFieldEditor
doesn't do anything differently from UIFieldEditor
yet. We'll need to dynamically add a method toMyFieldEditor
, but before we can do that, we need to write the method.Our needs are actually quite simple; when
-[MyFieldEditor deleteBackward]
is called, we want to call a method letting someone know that a backward deletion happened. Ideally, that "someone" would be the text field itself. Then we want to call through to the superclass implementation. Here's my implementation:
- (void)fieldEditor_deleteBackward { MyTextField *textField = objc_getAssociatedObject(self, BackwardDeleteTargetKey); [textField my_willDeleteBackward]; // Call through to super Class superclass = class_getSuperclass([self class]); SEL deleteBackwardSEL = @selector(deleteBackward); IMP superIMP = [superclass instanceMethodForSelector:deleteBackwardSEL]; superIMP(self, deleteBackwardSEL);}
It's really quite simple. We get a reference to the text field using ObjC associated objects, and call
-my_willDeleteBackward
on it. Then we pass the -deleteBackward
method up to the superclass,UIFieldEditor
. We have to use the runtime methods to do the superclass call because of the dynamic subclassing game; otherwise, we'd get the wrong superclass.I'm a little nervous about unilaterally changing the behavior of
UIFieldEditor
, because it seems likely thatevery text input area in your app uses the same instance of the field editor. So we do a little dance inMyTextField
's implementations of -becomeFirstResponder
and-resignFirstResponder
. It looks like this:- (BOOL)becomeFirstResponder { BOOL shouldBecome = [super becomeFirstResponder]; if (shouldBecome == NO) { return NO; } Class myFieldEditorClass = objc_lookUpClass([SubclassName UTF8String]); if (myFieldEditorClass == nil) { myFieldEditorClass = registerMyFieldEditor(); } id fieldEditor = [self valueForKey:@"fieldEditor"]; if (fieldEditor && myFieldEditorClass) { object_setClass(fieldEditor, myFieldEditorClass); objc_setAssociatedObject(fieldEditor, BackwardDeleteTargetKey, self, OBJC_ASSOCIATION_ASSIGN); } return YES;}
- (BOOL)resignFirstResponder { BOOL shouldResign = [super resignFirstResponder]; if (shouldResign == NO) { return NO; } id fieldEditor = [self valueForKey:@"fieldEditor"]; if (fieldEditor) { objc_setAssociatedObject(fieldEditor, BackwardDeleteTargetKey, nil, OBJC_ASSOCIATION_ASSIGN); Class uiFieldEditorClass = objc_lookUpClass("UIFieldEditor"); if (uiFieldEditorClass) { object_setClass(fieldEditor, uiFieldEditorClass); } } return YES;}
With this implementation, the shared
UIFieldEditor
instance will only be of classMyFieldEditor
while the text field is actively the first responder. As soon as the text field resigns, it goes back to being a regular oldUIFieldEditor
. No other text field in the app will be affected, and this text field will hear about all the backward deletion calls as soon as they come in.This is about the point where everyone working on
UIKit
starts squirming vigorously. If you'd like people tonot do this kind of stuff (which I'd heartily agree with), then let me direct your attention to Radars#10265826 and #10377565.In the meantime, use with caution. I haven't tried it in the App Store, but I suspect it will pass validation as there are no symbols referencing any private API in this implementation.
The code is on GitHub.
- This is actually the same mechanism Cocoa uses to implement Key-Value Observing; when you start observing a property of some object, Cocoa generates a new subclass of that object's class and implements a setter method that wraps your own with calls to
-willSetValueForKey:
and-didSetValueForKey:
. When all observers on an object are gone, its class is set back to the original class. ↩
- IOS重载私有类中的方法
- 缺陷:“重载”私有方法
- java 私有方法能否重载
- iOS访问类的私有成员变量及私有方法
- iOS访问类的私有成员变量及私有方法
- Python类中的 私有变量和私有方法
- iOS查看类的私有方法
- 获取ios私有方法
- Python(私有变量)类中的特殊方法
- Python(私有变量)类中的特殊方法
- Python(私有变量)类中的特殊方法
- Python(私有变量)类中的特殊方法
- Python(私有变量)类中的特殊方法
- Python(私有变量)类中的特殊方法
- Python(私有变量)类中的特殊方法
- 怎样调用类中的私有方法
- oc中的私有方法
- OC中的私有方法
- hudson插件开发笔记
- libiconv on ios
- 使用SqlBulkCopy插入DataTable到数据库
- 使用LINQ查询成绩合格的学生,并按照成绩降序排序。
- 架构设计 例子和实践
- IOS重载私有类中的方法
- 解决 Error on adding indexing context central
- redis代码结构之一mem,bio
- Debug模式应用程序输出Debug调试信息
- 架构设计贵在务实
- sip
- 关于软件的架构设计
- java环境配置(win7,jdk1.6.0_10)笔记一
- 如何做一名优秀的产品经理总结经验分享