Parsing XML

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.

Creating a parser delegate

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
  }
}

Tip

The NSXMLParserDelegate class requires the object to also conform to NSObjectProtocol. The easiest way to do this is to subclass NSObject. Note that the first mentioned supertype is the superclass; the second and subsequent supertypes must be protocols.

Downloading the data

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.

Tip

Make sure that both the download operation and the parsing are handled off the main thread, as both of these operations might take some time. Once the data is downloaded it can be parsed, and after it is parsed the UI can be notified to redisplay the contents.

Parsing the data

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
  }
}

Tip

The values of the namespaceURI, qualifiedName, and attributes might be nil. If they are not declared as implicitly unwrapped optionals, then the parser will fail with an EXC_BAD_ACCESS when calling the parse method.

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()
}

Tip

Note that the assignment of self to the delegate cannot be done until after the super.init has been called.

Now when the application is run, a set of news feed items will be displayed.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset