Although JSON is more commonly used, there are still many XML-based network services. Fortunately, XML parsing has existed in iOS since version 5 in the NSXMLParser
class and is simple to access from Swift. For example, some data feeds (such as blog posts) use XML documents such as Atom or RSS.
The NSXMLParser
is a stream-oriented parser; that is, it reports individual elements as they are seen. The parser calls the delegate
to notify when the elements are seen and have finished. When an element is seen, the parser also includes any attributes that were present; for text nodes, it includes the string content.
Thus, the parsing of an XML file involves some state management in the parser. The example used in this section will parse an Atom (news feed) file, whose (simplified) structure looks as follows:
<feed xmlns="http://www.w3.org/2005/Atom"> <title>AlBlue's Blog</title> <link href="http://alblue.bandlem.com/atom.xml" rel="self"/> <entry> <title type="html">Swift - classes></title> <link href="http://alblue.bandlem.com/2014/10/swift-classes-overview.html"/> ... </entry> ... </feed>
In this case, the goal is to extract all the entry
elements from the feed; specifically the title
and link
. This presents a few challenges that will become apparent later on.
Parsing an XML file requires creating a class that conforms to the NSXMLParserDelegate
protocol. To do this, create a new class, FeedParser
, that extends NSObject
and conforms to the NSXMLParserDelegate
protocol.
It should have an init
method that takes an NSData
and an items
property that will be used to acquire the results after they have been parsed:
class FeedParser: NSObject, NSXMLParserDelegate { var items:[(String,String)] = [] init(_ data:NSData) { // parse XML } }
The XML parser can either parse a stream of data as it is downloaded or it can take an NSData
object that has been downloaded previously. On successful download, the FeedParser
can be used to parse the NSData
instance and return a list of items.
Although individual expressions can be assigned temporary values similar to the last time, the statement can be written in a single line (although note that error handling is not present). Add the following to the end of the viewDidLoad
method of SimpleTable
:
session.dataTaskWithURL( NSURL(string:"http://alblue.bandlem.com/Tag/swift/atom.xml")!, completionHandler: {data,response,error -> Void in if data != nil { self.items += FeedParser(data).items self.runOnUIThread(self.tableView.reloadData) } }).resume()
This will download the Atom XML feed for the Swift posts from the author's blog at http://alblue.bandlem.com. Currently, the data is not parsed, so nothing will be added to the table in this step.
To process the downloaded XML file, it is necessary to parse the data. This involves writing a parser delegate to listen for the title
and link
elements. However, the title
and link
elements exist both at the individual entry
level and also at the top level of the blog. Therefore, it is necessary to represent some kind of state in the parser, which detects when the parser is inside an entry
element to allow the correct values to be used.
Elements are reported with the parser:didStartElement:
method and the parser:didEndElement:
method. This can be used to determine whether the parser is inside an entry
element by setting a boolean value when an entry
element starts and resetting it when the entry
element ends. Add the following to the FeedParser
class:
var inEntry:Bool = false func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String!, qualifiedName: String!, attributes: NSDictionary!) { switch elementName { case "entry": inEntry = true default: break } }
The link
stores the value of the references in an href
attribute of the element. This is passed when the start element is called, so is trivial to store. At this point, the title
might not be known, so the value of the link
has to be stored in an optional field:
var link:String? ... // in parser:didStartElement method case "entry": inEntry = true case "link": link = attributes.objectForKey("href") as String? default break;
The title
stores its data as a text node, which needs to be implemented with another boolean flag, indicating whether the parser is inside a title
node. Text nodes are reported with the parser:foundCharacters:
delegate method. Add the following to the FeedParser
:
var title:String? var inTitle: Bool = false ... // in parser:didStartElement method case "entry": inEntry = true case "title": inTitle = true case "link": ... func parser(parser: NSXMLParser, foundCharacters string:String) { if inEntry && inTitle { title = string } }
By storing the title
and link
as optional fields, when the end of the entry
element is seen, the fields can be appended into the items
list, followed by resetting the state of the parser:
func parser(parser: NSXMLParser, didEndElement elementName: String, namespaceURI: String!, qualifiedName: String!) { switch elementName { case "entry": inEntry = false if title != nil && link != nil { items += [(title!,link!)] } title = nil link = nil case "title": inTitle = false default: break } }
Finally, having implemented the callback methods, the remaining steps are to create an NSXMLParser
from the data passed in previously, set the delegate
(and optionally the namespace handling), and then to invoke the parser:
init(_ data:NSData) { let parser = NSXMLParser(data: data) parser.shouldProcessNamespaces = true super.init() parser.delegate = self parser.parse() }
Now when the application is run, a set of news feed items will be displayed.