Simple keyboard/text field interaction handling on iOS

EDITED TO ADD 5:15 PM PST I updated the gist because I realized I wanted to have a block run whenever I finished adjusting the view and therefore I added a completionBlock property. I'm using this to scroll a UITableView that contains text fields: whenever the keyboard finishes adjusting then I scroll the UITableView so the cell being edited is at the bottom. It works pretty well!


RoadTrip has a couple of places where if you hold the phone in landscape the keyboard can cover up a text field you're trying to edit. It's always struck me as weird that iOS can't handle this for you automatically but it doesn't, so there's a little weird piece of code you have to write to rearrange views somehow. In both places in RoadTrip I wanted to do the same thing: simply slide the main view upwards so that the field being edited is just over the top of the keyboard. I even wrote a comment saying that I should make this code some sort of generic solution, but I never went back to do so.

While working on 1.6 I noticed that the new predictive text bar on iOS 8 caused a problem with that code, so I patched it up. While doing that I noticed that the API for keyboard position changes does something other than the documentation does: if you ask for the notification curve you're supposed to get a UIViewAnimationCurve enum but sometimes you get 7, which is really a bitfield you can assign to the proper parts of the UIViewAnimationOptions. General consensus on the web seems to be to just take the "UIViewAnimationCurve", bitshift it up 16 places, and slam it into the UIViewAnimationOptions but that seems awful fragile to me.

Anyway, I fixed it up for RoadTrip, made another mental note to extract that code and submitted it.

Yesterday was the day I really did. I rewrote it into Swift and made a simple little widget that you can just create, feed it a view, and it watches for the keyboard notifications. It's a pretty simple class overall and I threw up a gist with the source. Take a look if that sounds like something you'd like to have. It uses my logger, but you can easily remove those calls.

I'm not sure what the deal is with the damn HeaderDoc comments. Sometimes they work for me in Xcode's Quick Help, sometimes they don't and I can't find anything that makes a difference. If anybody knows how to use HeaderDoc to make comments in Swift framework code that works reliably in both Objective-C and Swift let me know. I can't find anything about it.

The Last (I Hope) Word on NSMutableCharacterSet

Edited to add 2014-09-05: Xcode 6 beta 7 emits a warning for assigning the NSCharacterSet to a NSMutableCharacterSet *. I've never received feedback from Apple on the matter but since it's caught now I marked the radar as resolved.

Edited to add 2014-03-07: Filed a Radar for this bug. I can't link to that, but I also filed it on [Open Radar](https://openradar.appspot.com/radar?id=5874203416854528).

Edited to add: Reading the code snippets in here is difficult. I can't find a quick & easy way to make them have horizontal scrollbars if I use a pre tag around it. Rather that have them be truncated I removed the pre tags for now so they word wrap. I'll bug Squarespace about it later.

This NSMutableCharacterSet bug! I keep poking at it, going "OK I guess that makes sense" and then a day later going "Wait. That can't be right." Quick review. The following line of code:

NSMutableCharacterSet* fakeMutableSet = [NSCharacterSet decimalDigitCharacterSet];

seems to work. But although the object you get will respond to NSMutableCharacterSet selectors it won't actually do anything. Specifically you can write this:

[fakeMutableSet formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];

formUnionWithCharacterSet returns a void so you can't tell if it worked or not, and in fact it will not work. Conceptually now I should have a set with the decimal digits and whitespace. What I have (as far as I can tell, you can't probe much into NSCharacterSets) is the exact same set I had before this call. If you want a mutable copy you have to call mutableCopy like so:

NSMutableCharacterSet* mutableSet = [[NSCharacterSet decimalDigitCharacterSet] mutableCopy];

So far so good. I understand why I need mutableCopy, but at first I didn't understand why A ) fakeMutableSet wasn't mutable and B ) given that it wasn't mutable calling formUnionWithCharacterSet didn't cause an unrecognized selector assert. Seems like either it should be mutable or it shouldn't respond to the selector.

I finally got around to writing some test code and I've found enough that I'm convinced there is a real bug here and it's not at all what I suspected at first. I'll write this up in a radar after I finish this post. I've placed the source on GitHub if you want to follow along.

The full test code does a bunch of introspection on a variety of NSCharacterSet and NSMutableCharacterSet objects. But here's the smoking gun part:

NSCharacterSet* decimalSet = [NSCharacterSet decimalDigitCharacterSet];
NSMutableCharacterSet* fakeMutableSet = [NSCharacterSet decimalDigitCharacterSet];
NSMutableCharacterSet* mutableSet = [[NSCharacterSet decimalDigitCharacterSet] mutableCopy];

All three of these end up pointing at an instance of an internal class called _NSCFCharacterSet. So yeah. The docs say NSMutableCharacterSet is derived from NSCharacterSet but not really in the sense I think of it. And guess what: _NSCFCharacterSet responds to the NSMutableCharacterSet methods! See:

// So here's the catch right? _NSCFCHaracterSet can respond to formUnionWithCharacterSet but

// *DOESN'T WORK* if the underlying data is not mutable.

if ([[decimalSet class] instancesRespondToSelector:@selector(formUnionWithCharacterSet:)]) {

NSLog(@"Attempting union with decimal set");

// Have to cast this call to compile, but note no cast is needed for fakeMutableSet

// Also note this is NOT an unrecognizedSelector: we respond to this method

[(id)decimalSet formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];

}

Admittedly, yes I have cast decimalSet so the compiler does not complain but I expected the runtime to skip the call because the instances shouldn't respond to the selector. Somewhere up inside formUnionWithCharacterSet there is code that says "if I'm not mutable just sort of fail silently." It would have saved me a ton of time if that had at least logged a line that says "this call didn't work".

So yeah, if NSMutableCharacterSet objects were really a different class than NSCharacterSet objects I would have had a compiler error when I wrote the offending line without the mutableCopy. If NSCharacterSet objects didn't have methods to responds to selectors from NSMutableCharacterSet then I would have had a runtime assert. The NSCharacterSet and NSMutableCharacterSet API's sort of falsely represent what the data does. I don't see anyway to catch this happening from my code. So if you're using NSMutableCharacterSet be careful.

Does this extend to other NSMutable* classes? I don't know. These are the only ones I use where there are a lot of "return a pre-populated instances" method so I'd have more trouble making a real world example using NSMutableArray or NSMutableDictionary.

More iOS Utility Categories!

The more I think about that whole NSMutableCharacterSet thing I'm not happy with my current understanding. I may dig into that more in the near future. But at the moment I'm on my tear of pulling some common/utility code that I use in both Road Trip and Combat Imp into a common repository and putting a Creative Commons license on it. Today's code is adjacent to the NSMutableCharacterSet issue, but not directly related so I can dodge the "Why didn't this crash before I fixed it?" question for now. Quick summary of the problem:

I have many UITextFields where I want "numeric" values entered. I say "numeric" in quotes because it's quite a bit more complex than that. In Road Trip I need to accept strings like "$ 12,345.67". In Combat Imp I have some fields that are "just" numeric (round counter) and some that can take + and - signs (hit points, initiative modifier). All the fields need to take control characters or you can't backspace (and with an external keyboard you can hit things like Shift+Left Arrow to do selection.)

Shameful confession time: my original algorithm for this built a big list of all the characters I would ever want and then inverted the set. The reason for this is that I could call string rangeOfCharacterFromSet and find the first character in a string that was from a set. If the range returned anything other than NSNotFound I knew an illegal character was in the string and I could chuck it. This always seemed super backwards to me. I finally got around to doing what I wanted to do: I wrote a category for NSString that returns YES if all of the characters in the string are members of a set. Now instead of writing:

NSRange foundRange = [string rangeOfCharacterFromSet:nonlegalCharacters]];
if (foundRange.location != NSNotFound) {
    NSLog(@"String is %@, location is %d", string, foundRange.location);
    return NO;
} else {
    return YES;
}

Instead I build the character set of legal characters, cache that, and simply write:

return [string isContainedInCharacterSet:initiativeAndHPLegalCharacterSet];

(Both of these samples live inside textField:shouldChangeCharactersInRange:replacementString methods. Idea is when the user enters text I immediately validate the characters. If it's a 'g' and I only want numbers I reject it immediately. Doesn't even show up on screen.)

Much like the last stupid little helper category I plunked this in its own GitHub. Feel free to grab it if you need. You may notice that there are obvious other tests I could write it this category. I may in the future but for now, this is the only one I need. I'm not pretending to write some sort of comprehensive set of tests here, I'm just providing little snippets of code that I've found useful enough to isolate out and share in projects.

iOS Programming PSA

If you've read and used my post about converting currency strings on iOS you'll want to check the update I just posted. I had a nasty bug in RoadTrip under iOS 7 where it wouldn't accept the correct characters in the various cost fields. It turned out that when I wrote the code originally there was a line that says something like:

NSMutableCharacterSet* mutableSet = [NSCharacterSet decimalDigitCharacterSet];

At the time, this would give you y'know a NSMutableCharacterSet. Under iOS 7 it gives you a NSCharacterSet and calling a mutate method on it will just fail. Since they all return void you can't tell why it didn't work. (Or indeed, even tell that it didn't work. The symptom in my specific code was that not all of the characters I thought were in the set showed up there. There was no error or print statement or crash or anything.) Instead you need to write something like:

NSMutableCharacterSet* mutableSet = [[NSCharacterSet decimalDigitCharacterSet] mutableCopy];

I think (but can't confirm 100%) that what happened was in the early days of ARC that line invoked a copy operation and thus created a mutable set. But as time wore on somebody enhanced that and now it just references a prebuilt set. And the rest of the problem is just the way Objective-C works. I understand why it happens, but it wasn't something I expected, especially coming back into this code after doing C++ for a year.

Anyway, sorry if this bit anybody on the rear end!

UISplitViewController+QuickAccess

OK, let's switch gears and not talk about cats for a while shall we? Although fair warning, this post will only be interesting to somebody who does iOS programming. Hey sometimes that happens. Last week I sold my car so California could get it dismantled. If you don't want programming articles ask me about that sometime, and maybe I'll write a post about that.


OK, if you're still around you have some interest in code. Let's dive in. I have a few bits of code that are shared between the two iOS projects I'm working on currently. One is a dirt simple category that lets you quickly access controllers in an iPad UISplitViewController. In theory accessing the left controller is a single line of code, but it's a complicated line:

- (UIViewController*)leftController {
    return [[self.viewControllers objectAtIndex:0] topViewController];
}

And this is really the way I think of split views: there's a left controller and a right controller. I understand why that is represented as an array of UINavigationControllers but the whole point is that UISplitViewController should be an abstraction. When I think "OK, we need to notify the left ViewController that I just opened the map view so it can update the UI elements" I just want the left controller. I don't want to start thinking about stacks of controllers contained in an array and figure out the proper string of messages to get the left controller.

And I do that all over the place. I kept copying that damn string of messages around and every time the "Keep it DRY" birdie would hoot shamefully in my ear. So I finally got fed up enough to make a category to make UISplitViewControlller do what I want. Of course, this wasn't that DRY because I had two projects and they each had these source files but it was better.

Today I finally got around to sorting out how to put the category in GitHub as its own project, make a local repository, and then pull those files into both projects. This is actually the small code trial run for doing the same thing with some code that works around an iOS bug involving NSMutableCharacterSet. More on that later!

Anyway, if this category sounds useful to you I cleaned up the source, figured a modern Creative Commons license for it and put it on GitHub. Enjoy and let me know if you have any issues!