Ivan Ivan - 5 months ago 12
Swift Question

Parsing XML file in Swift (Xcode v 7.0.1) and retrieving values from dictionary

I'm trying to parse an XML file in Swift and I'm having a little bit of trouble understanding how the NSMutableDictionary works in storing values from elements.

The XML file looks like this,

<coord2 count="6">
<markers>
<marker>
<lat>36.99058167</lat>
<lng>-122.06620333</lng>
<timestamp>1444931620</timestamp>
<route>LOOP</route>
<id>7855</id>
<predictions>
,4,5,,6,7,,,,10,,,11,,11,,13,15,,14,,,16,,,0,,,2,,,,,,,,,,,,,,
</predictions>
<update_seconds>10</update_seconds>
<index>4</index>
</marker>
<marker>
<lat>36.99296</lat>
<lng>-122.06517333</lng>
<timestamp>1444934786</timestamp>
<route>UPPER CAMPUS</route>
<id>7860</id>
<predictions>
15,,,14,,,13,12,11,,10,9,,8,,7,,,6,,5,4,,3,2,,1,0,,,,,,,,,,,,,,,
</predictions>
<update_seconds>10</update_seconds>
<index>4</index>
</marker>
</markers>
<curr_time>1444931622</curr_time>
</coord2>


The way I'm attempting to parse the XML file is with the following code that I found from a tutorial at
http://www.theappguruz.com/blog/xml-parsing-using-nsxmlparse-swift

import UIKit
import GoogleMaps

class ViewController: UIViewController, NSXMLParserDelegate {

var parser = NSXMLParser()
var posts = NSMutableArray()
var elements = NSMutableDictionary()
var element = NSString()
var route = NSMutableString()
var timestamp = NSMutableString()

func beginParsing()
{
posts = []
parser = NSXMLParser(contentsOfURL:(NSURL(string:"http://xmfile.xml"))!)!
parser.delegate = self
parser.parse()
}

override func viewDidLoad() {
super.viewDidLoad()
beginParsing()

print("The route is")
let t = elements["route"]!
print(t)
print("The timestamp is")
let u = elements["timestamp"]!
print(u)
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

// didStartElement
func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String])
{
element = elementName
if (elementName as NSString).isEqualToString("marker")
{
elements = NSMutableDictionary()
elements = [:]
route = NSMutableString()
route = ""
timestamp = NSMutableString()
timestamp = ""
}
}

// foundCharacters
func parser(parser: NSXMLParser, foundCharacters string: String)
{
if element.isEqualToString("route") {
route.appendString(string)
} else if element.isEqualToString("timestamp") {
timestamp.appendString(string)
}
}

// didEndElement
func parser(parser: NSXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?)
{
if (elementName as NSString).isEqualToString("marker") {
if !route.isEqual(nil) {
elements.setObject(route, forKey: "route")
}
if !timestamp.isEqual(nil) {
elements.setObject(timestamp, forKey: "timestamp")
}
posts.addObject(elements)
}
}

}


Right now I'm only focusing on the "route" and "timestamp" elements just to gain an understanding of how parsing works. The output I get when I

let t = elements["route"]!
print(t)
print("previous is route")
let u = elements["timestamp"]!
print(u)
print("previous is timestamp")


is always the value of the last elements that have those names in the XML file. So, for the example XML file I provided the output of the 6 lines of code above would be

The route is

UPPER CAMPUS

The timestamp is

1444934786

I want to be able to distinguish between the multiple "marker" elements in the XML file and retrieve the values of elements within marker. In other words, be able to get the values of elements within the first marker also, not just the last one. How would I go about doing this?

Answer

NOTE I've put the whole thing in a gist which you can copy and paste into a playground.


Let's look at a simple example to get a start:

let xml = "<coord2 count=\"3\">"
+ "<markers>"
    + "<marker>"
    + "<item>marker1</item>"
    + "</marker>"
    + "<marker>"
    + "<item>marker2</item>"
    + "<lat>36</lat>"
    + "</marker>"
+ "</markers>"
+ "</coord2>"

A bit narrowed down, but Markers can have a item name (string) and lat value (int). A Coord2 will have an array of Markers, and a count (int) attribute.

To parse the above with custom classes, here's one approach.

First create a ParserBase class that does some ground work for us, namely accumulating foundCharacters so that it can be easily used by sub classes. Also (more importantly) it has a parent property which is used to hold references to the parent container class [this is used for the way in which we will be parsing XML].

// Simple base class that is used to consume foundCharacters
// via the parser

class ParserBase : NSObject, NSXMLParserDelegate  {

    var currentElement:String = ""
    var foundCharacters = ""
    weak var parent:ParserBase? = nil

    func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {

        currentElement = elementName
    }

    func parser(parser: NSXMLParser, foundCharacters string: String) {
        self.foundCharacters += string
    }

}

Since coord2 is our root tag, we will create a class that will map to that tag - it represents the root object, has an array of Markers, a count property, and is also the root delegate object for the XMLParser.

// Represents a coord2 tag
// It has a count attribute
// and a collection of markers

class Coord2 : ParserBase {


    var count = 0
    var markers = [Marker]()



    override func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {

        print("processing <\(elementName)> tag from Coord")

        if elementName == "coord2" {

            // if we are processing a coord2 tag, we are at the root
            // of this example
            // extract the count value and set it
            if let c = Int(attributeDict["count"]!) {
                self.count = c
            }
        }

        // if we found a marker tag, delegate further responsibility
        // to parsing to a new instance of Marker

        if elementName == "marker" {
            let marker = Marker()
            self.markers.append(marker)

            // push responsibility
            parser.delegate = marker

            // let marker know who we are
            // so that once marker is done XML processing
            // it can return parsing responsibility back
            marker.parent = self
        }
    }


}

The Marker class is as follows:

class Marker : ParserBase {

    var item = ""
    var lat = 0

    func parser(parser: NSXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {

        print("processing <\(elementName)> tag from Marker")

        // if we finished an item tag, the ParserBase parent
        // would have accumulated the found characters
        // so just assign that to our item variable
        if elementName == "item" {
            self.item = foundCharacters
        }

            // similarly for lat tags
            // convert the lat to an int for example
        else if elementName == "lat" {
            if let l = Int(foundCharacters) {
                self.lat = l
            }
        }

        // if we reached the </marker> tag, we do not
        // have anything further to do, so delegate
        // parsing responsibility to parent
        else if elementName == "marker" {
            parser.delegate = self.parent
        }

        // reset found characters
        foundCharacters = ""
    }

}

Now on to parsing, extracting info, and printing something.

let xmlData = xml.dataUsingEncoding(NSUTF8StringEncoding)!
let parser = NSXMLParser(data: xmlData)

let coord = Coord2()
parser.delegate = coord

parser.parse()


print("coord has a count attribute of \(coord.count)")
print("coord has \(coord.markers.count) markers")

for marker in coord.markers {
    print("marker item = \(marker.item) and lat = \(marker.lat)")
}

which outputs the following:

coord has a count attribute of 3
coord has 2 markers
marker item = marker1 and lat = 0
marker item = marker2 and lat = 36