Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
Saving data to the file system and reading it back is a pretty common operation. One of the first ways many Cocoa and iOS programmers learn to read and write are by using the convenience functions provided by NSArray
, NSData
, and friends. NSDictionary
has writeToFile:atomically:
or writeToURL:atomically:
for writing and dictionaryWithContentsOfFile:
or dictionaryWithContentsOfURL:
for reading.
The convenience calls assume that your data is in property list format (a.k.a. “plist”) and will automatically encode things for later retrieval. As a reminder, property list data means that you have a collection of objects of only these six types, or their mutable counterparts: NSString
, NSData
, NSNumber
, NSDate
, NSArray
, NSDictionary
. You can have an array of dictionaries with string keys and number and date values and still be considered a property list. Any other classes are forbidden. An NSColor
or UIBezierPath
in the dictionary will make the collection non-property-list.
These default reading and writing methods are kind of lame though. You can’t control the format the data is written – Mac does what Cocoa does. There’s also little-to-no error handling in many of the calls, so there’s not much feedback to tell you what’s wrong if someone accidentally (or purposely!) added a little Pony object to the collection when you weren’t looking. You’ll have to encode non-plist objects into a plist type (usually NSData
) to play in this playground. Also, mutability is lost. If you save a mutable array containing mutable strings, you’ll get back their immutable counterparts when re-read.
Cocoa has two “property list serialization” classes that work around these problems: NSPropertyListSerialization
(that class name is just cool to say), and NSJSONSerialization
.
NSPropertyListSerialization
takes plist objects and encode them into an NSData
, which you can write to a file, or send out a network connection, or something similar. The class can also take plist objects, encode them, and write them out an NSOutputStream
.
Going the other direction, you can hand NSPropertyListSerialization
an NSData
you got from somewhere, or give it an NSInputStream
with a stream of bytes from somewhere. If the decoding works, you get a pointer to the top-level plist object of the collection.
There are three formats used by the property list serialization class. There’s the the old “OpenStep” format, which looks a lot like JSON. Not terribly compact, but easy to read and edit. You can only read the OpenStep format – NSPropertyListSeialization
doesn’t support writing it. There’s an XML format, and a binary format. The XML format is human-readable and editable while the binary format is not.
Aside: you can convert an existing property list file between formats using the plutil utility. By default, preference property list files are saved in binary format. You can verify that, and then convert it:
% <strong>cd ~/Library</strong>
% <strong>file Preferences/com.apple.finder.plist</strong>
Preferences/com.apple.finder.plist: Apple binary property list
% <strong>plutil -convert xml1 Preferences/com.apple.finder.plist</strong>
% <strong>file Preferences/com.apple.finder.plist</strong>
Preferences/com.apple.finder.plist: XML document text
You can open this plist in TextEdit (or your favorite editor) to revel in its XML glory.
These formats correspond to the property list serialization constants NSPropertyListOpenStepFormat
, NSPropertyListXMLFormat_v1_0
, and NSPropertyListBinaryFormat_v1_0
.
Ok, so how do you use NSPropertyListSerialization
? It’s actually a class with nothing but class methods. You never interact with instances of NSPropertyListSerialization
.
To serialize a property list, you need a property list to serialize. The sample plist will be a dictionary with various plist types: (the complete code can be found at this gist)
static id makePlistObjects (void) {
NSMutableDictionary *top = [NSMutableDictionary dictionary];
[top setObject: @"Hi I'm a string" forKey: @"string"];
[top setObject: [NSNumber numberWithInt: 23] forKey: @"number"];
[top setObject: [NSNumber numberWithBool: YES] forKey: @"boolean"];
[top setObject: [NSDate date] forKey: @"date"];
NSArray *array =
[NSArray arrayWithObjects: @"I", @"seem", @"to", @"be",
@"a", @"verb", nil];
[top setObject: array forKey: @"array"];
NSDictionary *dict =
[NSDictionary dictionaryWithObjectsAndKeys:
@"Ack", @"Oop",
@"Bill the Cat", @"It's", nil];
[top setObject: dict forKey: @"dictionary"];
return top;
} // makePlistObjects
When NSLogged, it looks something like this:
2012-05-13 21:07:24.601 serial[57822:303] plist {
array = (
I,
seem,
to,
be,
a,
verb
);
boolean = 1;
date = "2012-05-14 01:07:24 +0000";
dictionary = {
Oop = Ack;
"It's" = "Bill the Cat";
};
number = 23;
string = "Hi I'm a string";
}
Once you have your property list object you can pre-flight it by using propertyList:isValidForFormat:
Here’s the start of a function to save the plist as XML:
static void saveAsXML (id plist) {
if (![NSPropertyListSerialization
propertyList: plist
isValidForFormat: kCFPropertyListXMLFormat_v1_0]) {
NSLog (@"can't save as XML");
return;
}
If you want to find out precisely why it can’t be saved, go ahead and try to encode it anyway and inspect the error that comes back. Usually it will be errors of the form “property lists cannot contain objects of type 'BNRPony'
”
To actually save in XML format, convert the property list to an NSData
:
NSError *error;
NSData *data =
[NSPropertyListSerialization dataWithPropertyList: plist
format: kCFPropertyListXMLFormat_v1_0
options: 0
error: &error;];
if (data == nil) {
NSLog (@"error serializing to xml: %@", error);
return;
}
Notice the error handling – always check the return value from the method, not the NSError
. If that sentence doesn’t mean much to you, or you actually test the value of the “NSError, then go, now, and read An NSError Error.
The NSData
now contains an encoded string which is the complete XML of the encoded property list. The arguments to dataWithPropertyList:...
are straightforward. Give it the top-level object of your property list object graph. Then a format constant, then zero for the options (it’s a place holder), and then the obligatory error-return pointer.
Once you’ve got the data, you can write it out. Here it is written to the current directory:
BOOL writeStatus = [data writeToFile: @"plist.txt"
options: NSDataWritingAtomic
error: &error;];
if (!writeStatus) {
NSLog (@"error writing to file: %@", error);
return;
}
} // saveAsXML
Most sane people will use something like NSSearchPathForDirectoriesInDomains
or URLsForDirectory:inDomains:
to get a good place to drop a file.
Saving in the binary format is exactly the same, except you’d use kCFPropertyListBinaryFormat_v1_0
.
Reconstructing your property list objects works the same way, just backwards. You read in the OpenStep, XML, or binary file from the file system, then use an NSPropertyListSerialization
class method to convert it into real live objects.
Here’s the counterpart function to saveAsXML
: given a file name, read it in and deserialize it. Here’s the reading of the file into an NSData
. Standard b-flat stuff:
static id readFromFile (NSString *path) {
NSError *error;
NSData *data = [NSData dataWithContentsOfFile: path
options: 0
error: &error;];
if (data == nil) {
NSLog (@"error reading %@: %@", path, error);
return nil;
}
And now the deserializing:
NSPropertyListFormat format;
id plist = [NSPropertyListSerialization propertyListWithData: data
options: NSPropertyListImmutable
format: &format;
error: &error;];
It’s a bit more complicated, but too much. You give it the data that holds the contents of file, along with some creation options. More on those in a bit. You pass the address of an NSPropertyListFormat
so you can find out what the original format was (if that’s important / interesting to you), as well as the address of an NSError
pointer to fill in if anything went wrong.
Here’s the error handling, and printing out of the file format:
if (plist == nil) {
NSLog (@"could not deserialize %@: %@", path, error);
} else {
NSString *formatDescription;
switch (format) {
case NSPropertyListOpenStepFormat:
formatDescription = @"openstep";
break;
case NSPropertyListXMLFormat_v1_0:
formatDescription = @"xml";
break;
case NSPropertyListBinaryFormat_v1_0:
formatDescription = @"binary";
break;
default:
formatDescription = @"unknown";
break;
}
NSLog (@"%@ was in %@ format", path, formatDescription);
}
And then the plist is returned from the function.
So what’s that NSPropertyListImmutable
parameter? That parameter controls the kinds of classes that are created when the property list is reconstituted. “Immutable” is the default behavior (if you pass zero). That means that all arrays will be NSArrays
, all Dictionaries will be NSDictionaries
, all strings will be NSStrings
, and so on, even if the original objects were mutable dictionaries, dictionaries, or strings.
You can also pass NSPropertyListMutableContainers
to turn all of the arrays and dictionaries into NSMutableArray
and NSMutableDictionary
instances. There’s also NSPropertyListMutableContainersAndLeaves
, which gives you mutable collections, as well as NSMutableString
s and NSMutableData
s. There’s no such thing as mutable NSNumbers
or NSDates
.
The mutability of these objects is not saved to the plist file. If you’re depending on mutability being preserved when writing to a plist, or something that’s plist-backed (like NSUserDefaults
), you’re going to break.
Immutable objects are generally safer (no need to worry about a value changing underneath you), so I prefer using those. But if you want to manipulate the results you can tweak the mutability. If you have an existing hierarchy of plist objects but with the wrong mutability, you can pass it through NSPropertyListSerialization
to change it, without ever going to the file system. Granted, you’ll have multiple copies of the data in memory.
The type of the mutability flags is kind of a wart in the API. propertyListWithData:...
declares the type of the options parameter as NSPropertyListReadOptions
, of which there are no values. NSPropertyListImmutable
and friends are actually NSPropertyListMutabilityOptions
s. They’re all enums, so it doesn’t really matter, but it’s one of the very few places you can’t trust what’s in the header.
NSJSONSerialization
is like NSPropertyListSerialization
, except it emits and consumes JSON rather than XML or binary. It’s a relatively new class, available starting in iOS 5 and Mac OS X 10.7. There are some limitations compared to NSPropertyListSerialization
, though. The object to encode has to be a collection – an NSArray
or NSDictionary
. You can’t use numbers as dictionary keys. You can’t save dates or data. But, you can save [NSNull null]
s, which you can’t do with property lists.
The calls are very similar to their property list serialization cousins. You can preflight:
if (![NSJSONSerialization isValidJSONObject: plist]) {
NSLog (@"can't save as JSON");
return;
}
Turn into an NSData
:
NSError *error;
NSData *data =
[NSJSONSerialization dataWithJSONObject: plist
options: NSJSONWritingPrettyPrinted
error: &error;];
JSON serializations has an option for writing – pretty printing it with newlines and indentation. Pass zero to get the more compact form.
After you’ve written and re-read it, you would reconstitute it with:
id plist = [NSJSONSerialization JSONObjectWithData: data
options: 0
error: &error;];
The options flags work differently than with property list serialization. Pass zero to get immutable containers, or bitwise-OR in these flags to pick and choose different features:
NSJSONReadingMutableContainers = (1UL << 0), // 0001
NSJSONReadingMutableLeaves = (1UL << 1), // 0010
NSJSONReadingAllowFragments = (1UL << 2) // 0100
Mutable containers gives you mutable arrays and dictionaries. Mutable leaves give you mutable strings. You can get mutable containers and leaves by passing NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves
. You can choose to have mutable leaves (NSMutableStrings
) and immutable containers, which you cannot do with NSPropertyListSerialization
.
Notice that Apple fixed the API wart here. These constants are of type NSJSONReadingOptions
, so there isn’t a specific mutability type.
The “allow fragments” option lets the JSON have a non-container being the top-level object coming in.
Now comes the big question. Should you actually use these calls?
I wouldn’t use them for my application’s document storage, unless my document really was nothing but a collection of property list objects. There’s better tech for that like NSCoding
/ Core Data / sqlite3 / etc.
That being said, property list (and JSON) files can be very handy for auxiliary files. Have a small database of stuff? Put it into a property list. You can edit it “for free” with the plist editor found in Xcode (or the stand-alone version). Have something configurable in your program? You can put its setting into a plist file, and then you can edit the file without having to recompile your program. (But you might want to use NSUserDefaults for that) The JSON calls make it pretty easy to pack stuff before sending to a web service, and unpacking the result.
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...