Thursday, November 17, 2011

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:

-------------------------------------------------
| 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 the UIKeyInput protocol, which UITextField conforms to via UITextInput). -[UITextField deleteBackward] is never called when the backspace key is pressed. Update: As of iOS 6.0, this solution works. You don't need any of the crazy stuff below. But feel free to read if you're interested, or if you still need iOS 5 support.
  • Subclassing UITextField and overriding various methods declared in the UITextInput protocol, such as -replaceRange:withText:, -setSelectedTextRange:, etc. Surprisingly, none of these are called either—despite declaring conformance to the UITextInput protocol, it seems that UIKit doesn't actually use any of those methods when handling keyboard input.
So, we've got a 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 since UITextField 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 at UIFieldEditor 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 subclassing UIFieldEditor 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:
  1. 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 from UIFieldEditor.
  2. We don't control the UIFieldEditor instance used by UIKit. Even if we could create our own MyFieldEditor, we still don't know how to swap out the existing shared object for our own.
Fortunately, we can handle both of these problems with the same solution: dynamic subclassing. We'll create our own subclass of 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 some great 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 existing UIFieldEditor. This requires using the runtime function object_setClass(id object, Class newClass). The newClass parameter is easy enough, but what are we going to pass it for the object? 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 to MyFieldEditor, 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 that every text input area in your app uses the same instance of the field editor. So we do a little dance in MyTextField'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 class MyFieldEditor while the text field is actively the first responder. As soon as the text field resigns, it goes back to being a regular old UIFieldEditor. 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 to not 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.

  1. 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.