The most popular mechanism to send structured data over a network is to encode it in JSON, which stands for JavaScript Object Notation. This provides a hierarchical tree data structure that can store simple numeric, logical, and string-based types along with the array and dictionary representations.
Both Mac OS X and iOS come with a built-in parser for JSON documents, in the NSJSONSerialization
class. This provides a means to parse a data object and return an NSDictionary
that contains the key/value pairs of a JSON object or an NSArray
to represent JSON arrays. Other literals are parsed and represented as the NSNumber
or NSString
values.
The JSON parser uses JSONObjectWithData
to create an object from an NSData
object containing a string. This is typically the format returned by network APIs and can be created from an existing string using dataUsingEncoding
with one of the built-in encoding types, such as NSUTF8StringEncoding
.
A simple JSON array of numbers can be parsed as follows:
let array = "[1,2,3]".dataUsingEncoding(NSUTF8StringEncoding)! let parsed = NSJSONSerialization.JSONObjectWithData( data:array, options:nil, error:nil)
The return type of this is an optional AnyObject
. The optionality represents the fact that the data content might not be valid JSON data. It can be cast to an appropriate type using the as
keyword; if there is a parsing failure, then an error will be thrown.
The options
can be used to indicate whether the return type should be mutable or not. Mutable data allows the caller to add or delete items after being returned from the parsing function; if not specified, the return value will be immutable. The NSJSONReadingOptions
options include MutableContainers
(containing data structures that are mutable), MutableLeaves
(the child leaves are mutable), and AllowFragments
(allow non-object, nonarray values to be parsed). Since these are bit flags, they can be combined with |
(the bitwise or operator). For example, to specify that both the containers and leaves should be mutable, .MutableContents|.MutableLeaves
should be used as the options
value.
The SampleTable.json
file (referred to in the viewDidLoad
method) stores an array of entries, with the title
and content
fields holding text data per entry:
[{"title":"Sample Title","content":"Sample Content"}]
To parse the JSON file and entries to the table, replace the default
clause in SampleTable
with the following:
default: let parsed = NSJSONSerialization.JSONObjectWithData( data, options:nil, error:nil) as NSArray! for entry in parsed { self.items += [(entry["title"] as String, entry["content"] as String)] }
Running the application will show the Sample Title and Sample Content entries in the table, which have been loaded and parsed from the book's GitHub repository.
If there are problems parsing the JSON data, then the return type of the JSONObjectWithData
function will return a nil
value. If the type is implicitly unwrapped then accessing the element will cause an error.
The error
is known as an inout argument; by passing a reference to an optional NSError
value, the function can assign an instance in addition to the normal return value:
var error:NSError? = nil if let parsed = NSJSONSerialization.JSONObjectWithData(data, options:nil, error:&error) { // do something with parsed } else { self.items += [("Error", "Cannot parse JSON (error!.localizedDescription)")] // show message to user }
The optional error
is passed into the function with an &
(ampersand) symbol. In C, this is used to pass the address of an object, but in Swift, this is limited to inout function parameters. It's mainly used for interoperability with existing C or Objective-C functions; generally using Option
to indicate errors is the preferred way in Swift. When the function returns, the error
will be not nil
if parsed
is nil
and vice versa.
The parsed
value will be of the type AnyObject?
, although the let
block will implicitly unwrap the value, known as
optional binding. In the previous section, the code was cast to an NSArray
directly, but if the returned result is of a different type (for example, an NSDictionary
or one of the fragment types such as NSNumber
or NSString
), then casting to an incorrect type will cause a failure.
The type of the object can be tested with if [object] is [type]
. However, since the next step is usually to cast it to a different class with as
, an as?
shorthand form can perform both the test and the cast in one step:
if let array = parsed as? NSArray { for entry in array { // process elements } } else { self.items += [("Error", "JSON is not an array")] }
A switch
statement can be used to check the type of multiple values at the same time. Since the values are optional NSString
objects, they need to be converted to a String
before they can be used in Swift:
for entry in array { switch (entry["title"], entry["content"]) { case (let title as String, let content as String): self.items += [(title,content)] default: self.items += [("Error", "Missing unknown entry")] } }
Now when the application is run, errors are detected and handled without the application crashing.