This is the first part of a multi-step process in which we will build a JSON parser in Swift.
In this first post we will see, among others, the following swift features in action:
- optionals
- enum with associated values
- curried functions
- typealias
We will do this only as an exercise to examine some Swift features. In fact many JSON libraries already exists and we could use them in a real app (see for example json-swift).
We begin now with a complete parser with full functionality. Then we will optimize it.
The data
Usually the data to parse comes from the web (i.e. from a REST service). To test our parser we will instead simulate it and use a simple file included in our project.
Open XCode and create a new “Single View Application” project. Name the product “ParserJson” and set the language to “Swift”.
Then download the following file, unzip and add it to the project:
The records in this file represent a list of photos, each one having the following columns:
- titolo (the title of the photo)
- autore (the photographer)
- latitudine (the geo-location of the photo)
- longitudine
- data (the date of the shot)
- descr (a description)
The Mapping
The aim of our parser is to convert the text from the file (JSON formatted) to something we can easily use in our program. This conversion is called “Mapping” and “something” will be an array of swift struct: structs usually are good to just contain data, but in Swift they can also methods, we will see this later.
We name this struct: Photo. So begin creating a new swift file (Photo.swift) and insert into it the following code:
1 2 3 4 5 6 7 8 9 |
struct Photo { let titolo : String let autore : String let latitudine : Double let longitudine : Double let data : String let descr : String } |
Design
We are writing a parser that reads data from a file but we want to design it in such a way that it will be very simple, in the future, to change the kind of source (the WEB, a database etc…).
We will not pass to the parser a simple string containing the JSON, but a function it has to invoke in order to obtain the data. This separate the logic of the parser from the “source” of the data.
Note that this function could return the data asynchronously, for example if it has to download it from the web. It is not happening when reading data from a file, but, again, our design will contemplate the more general case.
Our parser class will receive a function it has to call in order to receive the JSON string.
This is the implementation of the function readJsonFile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
func readJsonFile(jsonFileName : String, completion : (NSData?,NSError?)->()) { var fileData : NSData? var error : NSError? let filePath : String? = NSBundle.mainBundle().pathForResource(jsonFileName, ofType: "json") if let _filePath = filePath { fileData = NSData(contentsOfFile: _filePath, options:.DataReadingUncached, error: &error) } else { error = NSError(domain: "Parser", code: 100, userInfo: [NSLocalizedDescriptionKey:"The file was not found"]); } if (error != nil) { completion(nil, error) } else { completion(fileData, nil) } } |
The Optionals
Note the use of optionals: fileData can be nil (the operation can fail) so it must be declared NSData?. The same is true for error (if all works right actually it is nil) and it is declared as NSError?.
To tell to the completion handler the success or failure of the operation it must receive two parameters: NSData? and NSError?, two optionals.
Enum with associated value
The two optionals passed to the completion are really mutual. If one is nil, the other is not nil, and viceversa. We would like to pass to it a single parameter telling all the story.
Welcome to the enum with associated values:
1 2 3 4 |
enum ReaderResult { case Value(NSData) case Error(NSError) } |
If the enum evals to Value we are interested in the NSData. If the enum evals to Error we are interested in the NSError.
Using this enum we can modify the readJsonFile in the following way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func readJsonFile(jsonFileName : String, completion : ReaderResult->()) { var fileData : NSData? var error : NSError? let filePath : String? = NSBundle.mainBundle().pathForResource(jsonFileName, ofType: "json") if let _filePath = filePath { fileData = NSData(contentsOfFile: _filePath, options:.DataReadingUncached, error: &error) } else { error = NSError(domain: "Parser", code: 100, userInfo: [NSLocalizedDescriptionKey:"The file was not found"]); } var result : ReaderResult if (error != nil) { result = ReaderResult.Error(error!) } else { result = ReaderResult.Value(fileData!) } completion(result) } |
Note that we unwrap the error and fileData before passing it to the enum because the associated values are non optionals in our definition of the enum.
We will see shortly how to switch over the ReaderResult enum.
Curried function
In our design the Parser will call a reader function to obtain the data to process. Now we have a problem. The readJsonFile wants to know the name of the file to read, but the parser doesn’t know it (and it must not know, it is a generic parser).
One possible solution would be to pass the file name to the parser that, in turn, passes it to the reader function. But this is not so nice. Try another solution: the curried function.
Let’s change slightly the prototype of readJsonFile leaving the body identical:
1 2 3 |
func readJsonFile(jsonFileName : String)(completion : ReaderResult->()) { ... } |
This syntax allows us to call the method with only one parameter instead of two. In that case we will have as return value another func with only one parameter in the prototype.
So we will call the parser using the following code:
1 2 3 |
let parserTestReader = readJsonFile("test") // this is a func let parser = Parser() parser.start(parserTestReader) |
Note that parserTestReader is a function of prototype:
1 |
func readJsonFile(completion : ReaderResult->()) |
(what Parser was really expecting). It is really a reader of test (test.json).
Enum switch
Our class Parser will have the following structure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Parser { func start(reader : (ReaderResult->())->()) { // read the file reader() { (result : ReaderResult)->() in switch result { case let .Error(error): self.handleError(error) // TO DO case let .Value(fileData): self.handleData(fileData) // TO DO } } } } |
The method start receive the data and then switch over the result handling the error or the data. Note the syntax let .Error … to use different parts of the enum in the different case of the switch.
typealias
The function called by the parser to receive data is so defined:
1 |
(ReaderResult->())->() |
it just take in a ReaderResult parameter and return void.
Let us define a shortcut to name it, in order to avoid repetition (and mistyping). We can do this with the typealias command:
1 |
typealias ParserReader = (ReaderResult->())->() |
nothing magic but now we can use ParserReader when we want to refer to this function prototype.
Handle data
Let us pass to the parser another callback to be called at every new photo parsed:
1 |
typealias ParserNewPhoto = (Photo)->() |
Thus the call to the parser will be:
1 2 3 4 |
parser.start(parserReader) { (photo : Photo) -> () in println(photo.data + ": " + photo.titolo) } |
The method start of the parser will receive this callback and pass it to the handleData method. The closure is the last parameter of the method so we can write it after the closed parenthesis. This is called a Trailing Closure in swift.
The new start implementation is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func start(reader : ParserReader, parserNewPhoto : ParserNewPhoto) { // read the file reader() { (result : ReaderResult)->() in switch result { case let .Error(error): self.handleError(error) case let .Value(fileData): self.handleData(fileData, parserNewPhoto) } } } |
The handleData method has the following implementaiton:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
func handleData(data : NSData, parserNewPhoto : ParserNewPhoto) { var error : NSError? let json : AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &error) if let _json = json as? [AnyObject] { for jsonItem in _json { if let _jsonItem = jsonItem as? [String: AnyObject] { let titolo : AnyObject? = _jsonItem["titolo"] let autore : AnyObject? = _jsonItem["autore"] let latitudine : AnyObject? = _jsonItem["latitudine"] let longitudine : AnyObject? = _jsonItem["longitudine"] let data : AnyObject? = _jsonItem["data"] let descr : AnyObject? = _jsonItem["descr"] if let _titolo = titolo as String? { if let _autore = autore as? String { if let _latitudine = latitudine as? Double { if let _longitudine = longitudine as? Double { if let _data = data as? String { if let _descr = descr as? String { let photo = Photo(titolo: _titolo, autore: _autore, latitudine: _latitudine, longitudine: _longitudine, data: _data, descr: _descr) parserNewPhoto(photo) } } } } } } } } } } |
The method JSONObjectWithData returns an object of type AnyObject? and we analyze it.
All the function is an optional binding over all the optionals.
We expect the json object to be an array. If it indeed is, we loop on the array. For each element we check if it is a dictionary and then optional bind the elements of the dictionary.
Only if all the optional binds are all successfull we create a new Photo object and pass it to the parserNewPhoto callback.
Error handling
Our error handling function simply report the problem in the console:
1 2 3 |
func handleError(error : NSError) { println(error.localizedDescription); } |
I’m sure you will implement a better one in your app.
Note that we are not handling error inside handleData. We will do this in the next post.
Summary
In this post we met some interesting swift features. The Parser is not finished. In the next post we will refactor it in a more swift way.
Code
The project of this post can be found on Github.