This column is a continuation from the last on how to work with strings in Cocoa. In the last column, we covered a lot of the basics about strings that you would learn with any programming language/environment.
We started with the essential step of first creating a string with the variety of methods provided to us by NSString, and then went on to learn how we can use these strings. We touched on some more OOP with class clusters, and also some very simple file I/O with strings. Additionally, much time was spent discussing comparison, search, and substring extraction methods.
Today, I want to continue along this course by telling you about NSString's path manipulation tools, and NSString's subclass, NSMutableString, which allows us to create strings whose contents we can edit after their creation -- something that was not possible with NSString alone. I will finally conclude this column with a bit of string miscellany. With that said, let's get started!
In the previous column I showed you how we can write strings to a text file, and create new strings from a text file. In these situations, we represented file locations (paths) as strings. This will also be the case any time we work with files, that is, the file paths will be NSString object strings.
The string representation of file locations and paths is standard in Cocoa, and the NSString class provides us with a set of methods that allows us to manipulate file path strings. These methods let us do things like replace the tilde in a path with an absolute path, resolve symbolic links, extract path components into new string, and work with file extensions.
|
Also in Programming With Cocoa: |
By now we all know Mac OS X is built on top of Unix (oh man, would I love to go on about how Apple scored big with that move!), and as such, directories and files are organized the Unix way, and manipulated in the Unix way. There are several things you should know about this that are different from the normal Macintosh way of doing things (I'm sure many of you know about this, but it never hurts to cover the fundamentals, so please bear with me).
In the Unix way of doing things, the path to a user's home directory is abbreviated with a tilde ("~"). This is a very useful shorthand for moving around in the file system. For example, my home directory on my lime green iBook, is /Users/mike/ in the expanded way of doing things, or in the shorthand it's simply ~. However, a tilde in a path string will not be recognized as a valid path, so we have to expand the tilde using the - stringByExpandingTildeInPath method. In the following example:
NSString *shortPath = @"~/textFile.txt";
NSString *absolutePath = [shortPath stringByExpandingTildeInPath];
the - stringByExpandingTildeInPath method would return a string that is the expanded, absolute path to textFile.txt, with the current user's home directory path put in place of the tilde. The new path is now /Users/mike/textFile.txt. We can also accomplish the reverse, where a the path to a user's home directory is abbreviated with a tilde. The method we use to do this is -stringByAbbreviatingWithTildeInPath:
NSString *path = [absolutePath stringByAbbreviatingWithTildeInPath];
which would set path to ~/textFile.txt. Notice that in all of these methods (and all the ones we talked about last time) the first word is "string", which means a string is returned by the receiver of any string… message. This syntax is common throughout the Cocoa frameworks and we will see it with arrays, numbers, dictionaries, and so on. Cocoa's method names are designed to be as unambiguous as possible (this does make for more typing, but I think readable, unambiguous code is worth it).
File extensions are also prevalent in Unix (to many a Mac user's disdain). The file extension is the one, two, three, or more letter suffix following a file's name with a period in between the name and the extension (the single period is not considered to be a part of the extension in Cocoa). We can obtain the file extension as a new string object by invoking the - pathExtension method on some path string, which returns an NSString objects containing the path extension, less the period.
NSString *path = @"~/textFile.txt";
NSString *pathExtension = [path pathExtension];
The string pathExtension would be "txt". The period is removed. If there is no period indicating a path extension, an empty string is returned. If there is no file, an empty string is also returned.
Naturally, there's a method to do the reverse; that is, add a path extension where there once was none. This is done using the - stringByAppendingPathExtension: method. If we had a path string /Users/mike/textFile, we could add the ".txt" path extension by doing the following:
NSString *path = @"Users/mike/textFile";
path = [path stringByAppendingPathExtension:@"txt"];
The path would then be /Users/mike/textFile.txt. Note that in this example we stored the returned path over the old path in the variable path.
If we wanted to get rid of the path extension altogether, leaving us with just a path and file name, we would use the method - stringByDeletingPathExtension. Using our original path /Users/mike/textFile.txt:
path = [path stringByDeletingPathExtension];
the path would then be /Users/mike/textFile. The method returns the original string, less the path extension (including the period directly preceding the path extension).
Another useful set of path manipulation tools in NSString are those that let us work with the components of a path, the individual directory names, and the file name at the end. The first of these methods is -pathComponents. Seems easy enough. What this does is take a path string and split it up at each of the slashes into several strings, which are then put into an NSArray object. We haven't talked about NSArray yet, but it's nothing more than a standard array, Cocoa style. So, if we had the following path:
NSString *thisColumn = @"/Users/mike/Documents/Cocoa_Column/Column8.doc"
(I admit it, I'm fairly fond of Word.) -pathComponents would split it up and dump it into an array like this:
NSArray *theComponents = [thisColumn pathComponents];
The resulting array looks like this:
| Array Index | Path Component |
| 0 | Users |
| 1 | mike |
| 2 | Documents |
| 3 | Cocoa_Column |
| 4 | Column8.doc |
This allows us to go in and completely pick apart a path with easy access to any of the directories or the file. If our desires are of a simpler nature, we can use any of the methods that let us work with the last component, which is usually enough. They are -lastPathComponent, -stringByAppendingPathComponent:, and -stringByDeletingLastPathComponent. They all work as you would expect them to from the name. If they were given the same path as above,
NSString *thisColumn = @"/Users/mike/Documents/Cocoa_Column/Column8.doc"
then we could do the following things with it:
NSString *lastComponent = [thisColumn lastPathComponent];
NSString *pathLessFilename = [thisColumn stringByDeletingLastPathComponent];
NSString *originalPath = [pathLessFilename stringByAppendingPathComponent:lastComponent];
In the end, originalPath is equivalent to thisColumn. What happened was in the first line we made a new string, which was the last path component returned by lastPathComponent (aren't these method names spiffy?) of thisColumn, "Column8.doc". In the next line, we created another string, which is the directory in which the Column8.doc lives, and then in the last line we combine the two to reconstruct the original path.
So, you've seen a small handful of the methods available for working with paths. You can see the whole lot of them in the NSString class reference; naturally, the class reference goes into much more detail than we've seen here.
Onward to those strings that can mutate!
|
|
|
Previously, in our discussion of NSString, I mentioned the fact that fundamentally strings in Cocoa are immutable arrays of Unicode characters. This means that once we create a string, we're stuck with it and there's nothing we can do about it with NSString anyway. NSMutableString is our solution. NSMutableString is a subclass of NSString that allows us to do exactly what we couldn't do before -- we can now create strings that posses the ability to modify and edit, or mutate, their contents.
Because NSMutableString is a subclass of NSString, everything we learned about strings in the previous column and this one holds true for mutable strings as well. In this section, I want to walk you through all of the methods that NSMutableString adds to NSString that allow us to edit the contents of mutable strings (I will refer to instances of NSString simply as strings, and instances of NSMutableString as mutable strings).
We have two methods that allow us to append a string to the end of an existing mutable string: They are - appendString: and - appendFormat:, and they work in the following way.
NSMutableString *aMutableString = [[NSMutableString alloc] initWithString:@"Hello"];
[aMutableString appendString:@", World!"];
The appendString message to aMutableString tells aMutableString to add the argument of the message to the end, so the resulting value of aMutableString is "Hello, World!". There are two things here to notice. First, we cannot use the @"..." construct to directly create mutable strings. Remember that this construct created strings that are compiled and are always present. Obviously we cannot change a pre-compiled string, so we have to create a new NSMutableString object like any other object, using alloc and an init method. We can however, use any of the init... methods declared in NSString, because that is the parent class of NSMutableString.
Additionally, note that none of the content-editing methods of NSMutableString return a value, their return type is void. These methods actually do change the value of the receiver string, rather than returning a modified copy of it, as we've become accustomed to with NSString methods.
We can also append a formatted string to the end of an existing string using -appendFormat:. This works exactly the same as the previous method, except you can format the string according to the rules we learned in the last column.
The method - deleteCharactersInRange: accepts a range (NSRange data type) as the sole argument and removes from the receiver string the characters that fall in that range. To make a range, the Foundation Framework provides the function (not a method of any class, a true C-type function) NSMakeRange(unsigned int location, unsigned int length), which takes two arguments that are the components of a range, and returns an NSRange variable. The first argument is the index of the first letter (think arrays -- counting starts from 0) and the second argument is the length of the range, inclusive of the first character. We could then remove the string we added in the previous example so we are left with the original string, "Hello":
NSRange aRange = NSMakeRange(5, 8);
[aMutableString deleteCharactersInRange:aRange];
Moving along with the methods of NSMutableString, we have - insertString: atIndex: which inserts the string given in the first argument into an index of the receiver given in the second argument. The characters in the receiver string that are at specified index and beyond all get displaced toward the end to make room for the inserted string. We would use this method like this:
NSMutableString *aMutableString = [[NSMutableString alloc] initWithString:@"Hello, World!";
[aMutableString insertString:@" (not goodbye)" atIndex:5];
which would transform aMutableString into "Hello (not goodbye), World!". Note that the first character of the word we are inserting is a space. Remember that arrays are indexed starting at index 0, so the "H" in aMutableString is at index 0.
If we want to completely change a string from one value to another, we can use the -setString: method. So we can take out the resulting string, aMutableString, from above and change it to whatever using this method:
[aMutableString setString:@"whatever."];
which makes aMutableString now hold the value "whatever.". This method should remind you of the set... methods we learned about in NSControl.
The final method we have in our mutable string toolbox is - replaceCharactersInRange: withString:, which does exactly what the method name says it will do. The first argument is a range indicating the characters you want to remove from the receiver string, and the second argument is a string which you want to put in their place -- just a straightforward replacement operation. Let's see how this works in code:
NSMutableString *aString = [[NSMutableString alloc] initWithString:@"Hello, World!";];
[aString replaceCharactersInRange:NSMakeRange(7,5) withString:@"Universe"];
And our original string "Hello, World" is changed to "Hello, Universe!".
Before the end of this column, I want to go over a few features of NSString that allow you to modify the case of a string in addition to obtaining numeric values from a string.
In the article about the color meter, we talked about the ability to take numerical values from an interface control by sending the object any of the following messages: intValue, doubleValue, and floatValue. The same is true for strings. That is, NSString responds to these exact messages in the same way as we learned for the interface controls (think polymorphism). The catch is that the string object can only be a number for these methods to work. We can't use these methods to pick a number out of a string. We can, however, find a number in a string, and then return its C-typed value. Suppose we have a string which has some text, and then a number after the text filling the remainder of the string. We could get it out in the following way:
NSString *textAndNumbers = @"Number of eggs in a dozen= 12";
NSString *numberPart = [textAndNumbers substringFromIndex:27];
int numberInADozen = [numberPart intValue];
I admit, this example is a bit contrived, but I imagine you get the idea here. Remember, we have to use the correct ...Value method according to the type of number in the string, and the destination data type.
One last tidbit of NSString madness I want to share with you are the methods that let us change the case of the text in the string: -capitalizedString, -lowercaseString, and - uppercaseString. -lowercaseString returns a string where every character of the receiver is in lowercase, -uppercaseString does the same except every character is converted to uppercase. The last of these three, -capitalizedString, is a little fancier, returning a string where the first letter of each word in the receiver string is converted to its uppercase counterpart. Here are these methods in action:
NSString *string = @"tHe uniVERSity of TEXAS";
NSString *lcstring = [string lowercaseString];
NSString *ucstring = [string uppercaseString];
NSString *capstring = [string capitalizedString];
These last three lines would convert string to "the university of texas", "THE UNIVERSITY OF TEXAS", and "The University Of Texas", in that order.
In this column we just scratched the surface of Cocoa's string-handling ability. I've mentioned throughout the column that you should check out the NSString class reference (I hope you have the Foundation and Application Kit class reference pages bookmarked by now!) to get more detailed information about the other methods not discussed here. What I wanted to show you here are the basic ways of working with strings, giving you the confidence and the know-how to dig deeper into the dustier, more advanced parts of the NSString class. In the next column we will set ourselves to the task of learning how to develop good memory management habits.
Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.
Read more Programming With Cocoa columns.
Return to the Mac DevCenter.
Copyright © 2009 O'Reilly Media, Inc.