-[NSString containsString:]
was added in iOS 8. If you’re writing a standard library, this seems like one of the most crucial components for string handling. And yet, it’s been conspicuously absent since the early ’90s, when NSString was created.
Apple gives you a function called -rangeOfString:
whose functionality is a superset of this behavior. You can use a short snippet of code with -rangeOfString:
in the place of -containsString:
:
if ([aString rangeOfString:searchString].location != NSNotFound) {
//substring is found
} else {
//substring is not present
}
Writing it this way is very tough to read, and code is read much more often than it is written. “The location of the searchString inside aString is not equal to ‘not found’” is a tough thing to parse from code to English, especially because of the double negative.
Of course, you’re currently screaming at me, “Just use a category!” And it’s trivial to add this method to the NSString
class. I can do it, and you can too.
- (BOOL)containsString:(NSString *)searchString {
return [self rangeOfString:searchString].location != NSNotFound;
}
This is an unsatisfactory solution. There are three primary reasons why.
First, developers are lazy. If you search GitHub for rangeOfString
, you will find pages and pages of open source repositories that use -rangeOfString
almost exclusively in the place of -containsString:
. Why didn’t they just write the category for the message they needed? There’s a lot of cognitive load to adding a file to your project. It also involves a context switch: instead of just writing the code you want in the place where you want it, you have to jump to a different place to add a different thing. The out-of-order nature of that kind of programming makes it very hard to do the right thing.
Secondly, category methods must either be prefixed — and ugly — or unprefixed — and go against Apple’s best practices. Personally, I see prefixing as unnecessary. How many ways are there to write -containsString:
? How many ways are there to write -firstObject
? Nevertheless, I worry about method names -select:
that could easily conflict (imagine a method that selects objects in a collection based on a block, like Ruby’s Enumerable #select
, or an IBAction routed through the responder chain, like -copy:
or -selectAll:
). The important thing here is that, whether or not I think prefixing is ugly, I also don’t like going against best practices. There isn’t really a way out of this trap. My code will be unsatisfactory either way.
Lastly, writing the method yourself in a category will always be less performant than the vendor’s version. While I was writing my library Objective-Shorthand, I did some research in the runtime on -[NSArray reversedArray]
. I found that even though it’s not a public method, the SDK does actually respond to this message. It returns an __NSArrayReversed
, which is ostensibly an class-clustered NSArray
that accesses objects in reverse order. Most implementations of array reversing (including mine!) would call allObjects
on the reverseObjectEnumerator
, which creates a full copy of the array, the obvious and simple solution. Apple’s implementation cleverly sidesteps this. You could imagine similar optimizations for other methods. Why this method isn’t public, I don’t know.
In using categories, the best we can do is bundle up all these obvious shortcomings into a single library, and use that one dependency. This is exactly what Objective-Shorthand attempts to do. And even in this “best” solution, we still have to import Objective-Shorthand everywhere we need it, or include it in the .pch file (which is probably an anti-pattern, since it makes your dependencies implicit).
There are other reasons as well why the bundling of all these categories together doesn’t work. I did a quick proof-of-concept for a small state machine library a few weeks ago, and while Objective-Shorthand would have made my work a lot easier, I couldn’t justify making other developers dependent on Objective-Shorthand, just to make my coding experience a little nicer.
So given these myriad reasons why categories aren’t good enough, why will Apple not implement these methods? One common reasoning I’ve heard is that Apple’s conservatism causes a reticence for API additions. Mattt Thompson asked an engineer at a WWDC lab why it took so long to add JSON support to the SDK:
Apple is a company with a long view of technology. It’s really difficult to tell whether a technology like JSON is going to stick, or if it’s just another fad. Apple once released a framework for PubSub, which despite not being widely known or used, still has to be supported for the foreseeable future. Each technology is a gamble of engineering resources.
This argument isn’t very compelling! -containsString:
isn’t a new technology, nor is its usage in doubt. Our simple GitHub search from earlier shows that. Also, don’t forget that this is the same vendor that added a dozen methods to NSString
for manipulating paths. Conservative? Doubtful!
Other vendors have proven that it’s possible to do this well. Ruby, for example, takes it to the other extreme: their Integer objects respond to methods like #odd?
, #even?
and even #gcd
. Providing those methods in the standard library yields more readable code and it gives the vendor the opportunity to tweak and optimize them at later date, knowing that anyone who needs them will benefit from their optimizations.
All of the solutions available to developers — the unreadable -rangeOfString:
dance, a utility object, small categories copied to each new project, or compiling them into one library like Objective-Shorthand — are insufficient.
In the same way that base types like Swift’s Optional or JavaScript ES6’s Promises must be built-in for proper adoption, these simple methods need to be available to all developers. Apple’s proclivity towards leaving holes in their API forces developers into a bind. We must either write inexpressive, hard-to-read code, do needless work opening these classes back up, or leave a trail of dependencies in our wake.