Parsing XML from URL in Swift

Rob's answer for Swift 3 / 4

func getDataFrom(url: URL, completion: @escaping (_ data: Data?, _ error: Error?)->()) {

        let session = URLSession(configuration: .default)

        let download = session.dataTask(with: url) { data, response, error in

            completion(data, error)

        }

        download.resume()

}

getDataFrom(url: url) { data, error in

        guard let data = data else {
            return
        }

        let parser = XMLParser(data: data)
        parser.delegate = self

        if parser.parse() {

            print(self.results)

        }

}

// a few constants that identify what element names we're looking for inside the XML

let recordKey = "record"
let dictionaryKeys = ["EmpName", "EmpPhone", "EmpEmail", "EmpAddress", "EmpAddress1"]

// a few variables to hold the results as we parse the XML

var results: [[String: String]]!         // the whole array of dictionaries
var currentDictionary: [String: String]! // the current dictionary
var currentValue: String?  

// start element
//
// - If we're starting a "record" create the dictionary that will hold the results
// - If we're starting one of our dictionary keys, initialize `currentValue` (otherwise leave `nil`)

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

    if elementName == recordKey {

        self.currentDictionary = [String : String]()

    } else if dictionaryKeys.contains(elementName) {

        self.currentValue = String()

    }

}

// found characters
//
// - If this is an element we care about, append those characters.
// - If `currentValue` still `nil`, then do nothing.

func parser(_ parser: XMLParser, foundCharacters string: String) {

    self.currentValue? += string

}

// end element
//
// - If we're at the end of the whole dictionary, then save that dictionary in our array
// - If we're at the end of an element that belongs in the dictionary, then save that value in the dictionary

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

    if elementName == self.recordKey {

        self.results.append(self.currentDictionary)

        self.currentDictionary = nil

    } else if dictionaryKeys.contains(elementName) {

        self.currentDictionary[elementName] = currentValue

        self.currentValue = nil

    }

}

// Just in case, if there's an error, report it. (We don't want to fly blind here.)

func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
    print(parseError)

    self.currentValue = nil
    self.currentDictionary = nil
    self.results = nil

}

The process is simple:

  1. Create XMLParser object, passing it the data.
  2. Specify the delegate for that parser.
  3. Initiate the parsing.

So, in Swift 3/4, that looks like:

let task = URLSession.shared.dataTask(with: url) { data, response, error in
    guard let data = data, error == nil else {
        print(error ?? "Unknown error")
        return
    }

    let parser = XMLParser(data: data)
    parser.delegate = self
    if parser.parse() {
        print(self.results ?? "No results")
    }
}
task.resume()

The question is how do you implement the XMLParserDelegate methods. The three critical methods are didStartElement (where you prepare to receive characters), foundCharacters (where you handle the actual values parsed), and didEndElement (where you save you results).

You asked how to parse a single record (i.e. a single dictionary), but I'll show you a more general pattern for parsing a series of them, which is a far more common situation with XML. You can obviously see how to simplify this if you didn't need an array of values (or just grab the first one).

// a few constants that identify what element names we're looking for inside the XML

// a few constants that identify what element names we're looking for inside the XML

let recordKey = "record"
let dictionaryKeys = Set<String>(["EmpName", "EmpPhone", "EmpEmail", "EmpAddress", "EmpAddress1"])

// a few variables to hold the results as we parse the XML

var results: [[String: String]]?         // the whole array of dictionaries
var currentDictionary: [String: String]? // the current dictionary
var currentValue: String?                // the current value for one of the keys in the dictionary

And

extension ViewController: XMLParserDelegate {

    // initialize results structure

    func parserDidStartDocument(_ parser: XMLParser) {
        results = []
    }

    // start element
    //
    // - If we're starting a "record" create the dictionary that will hold the results
    // - If we're starting one of our dictionary keys, initialize `currentValue` (otherwise leave `nil`)

    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
        if elementName == recordKey {
            currentDictionary = [:]
        } else if dictionaryKeys.contains(elementName) {
            currentValue = ""
        }
    }

    // found characters
    //
    // - If this is an element we care about, append those characters.
    // - If `currentValue` still `nil`, then do nothing.

    func parser(_ parser: XMLParser, foundCharacters string: String) {
        currentValue? += string
    }

    // end element
    //
    // - If we're at the end of the whole dictionary, then save that dictionary in our array
    // - If we're at the end of an element that belongs in the dictionary, then save that value in the dictionary

    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        if elementName == recordKey {
            results!.append(currentDictionary!)
            currentDictionary = nil
        } else if dictionaryKeys.contains(elementName) {
            currentDictionary![elementName] = currentValue
            currentValue = nil
        }
    }

    // Just in case, if there's an error, report it. (We don't want to fly blind here.)

    func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
        print(parseError)

        currentValue = nil
        currentDictionary = nil
        results = nil
    }

}

For Swift 2 rendition, see previous revision of this answer.


I wrote a pod for mapping XML to objects, called XMLMapper. (uses the same technique as the ObjectMapper)

For what you want to achieve you can simply use XMLSerialization class like:

let url = URL(string: "http://www.urlexample.com/file.xml")

let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
    do{
        let xmlDictionary = try XMLSerialization.xmlObject(with: data!) as? [String: Any]
    } catch {
        print("Serialization error occurred: \(error.localizedDescription)")
    }
}
task.resume()

You can also implement the XMLMappable protocol like:

class XMLResponse: XMLMappable {
    var nodeName: String!

    var record: Record?

    required init(map: XMLMap) {

    }

    func mapping(map: XMLMap) {
        record <- map["record"]
    }
}

class Record: XMLMappable {
    var nodeName: String!

    var empName: String!
    var empPhone: String!
    var empEmail: String?
    var empAddress: String?
    var empAddress1: String?

    required init(map: XMLMap) {

    }

    func mapping(map: XMLMap) {
        empName <- map["EmpName"]
        empPhone <- map["EmpPhone"]
        empEmail <- map["EmpEmail"]
        empAddress <- map["EmpAddress"]
        empAddress1 <- map["EmpAddress1"]
    }
}

And map the response XML into that objects by using XMLMapper class:

let xmlResponse = XMLMapper<XMLResponse>().map(XMLObject: xmlDictionary)

UPDATE: Cover @fahim-parkar's comment.

To map an object (or many objects in an array) you use the same technique.

For example to map the following XML:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
    <channel>
        <title>Apple Hot News</title>
        <link>http://www.apple.com/hotnews/</link>
        <description>Hot News provided by Apple.</description>
        <language>en-us</language>
        <copyright>Copyright 2016, Apple Inc.</copyright>
        <pubDate>Tue, 26 Apr 2016 15:53:26 PDT</pubDate>
        <lastBuildDate>Tue, 26 Apr 2016 15:53:26 PDT</lastBuildDate>
        <category>Apple</category>
        <generator>In house</generator>
        <docs>http://blogs.law.harvard.edu/tech/rss/</docs>
        <item>
            <title>Apple Reports Second Quarter Results</title>
            <link>http://www.apple.com/pr/library/2016/04/26Apple-Reports-Second-Quarter-Results.html?sr=hotnews.rss</link>
            <description>Apple today announced financial results for its fiscal 2016 second quarter ended March 26. The company posted quarterly revenue of $50.6 billion and quarterly net income of $10.5 billion, or $1.90 per diluted share. These results compare to revenue of $58 billion and net income of $13.6 billion, or $2.33 per diluted share, in the year-ago quarter. Gross margin was 39.4 percent compared to 40.8 percent in the year-ago quarter. International sales accounted for 67 percent of the quarter’s revenue. “Our team executed extremely well in the face of strong macroeconomic headwinds,” said Tim Cook, Apple’s CEO. “We are very happy with the continued strong growth in revenue from Services, thanks to the incredible strength of the Apple ecosystem and our growing base of over 1 billion active devices.”</description>
            <pubDate>Tue, 26 Apr 2016 14:44:21 PDT</pubDate>
        </item>
        <item>
            <title>Final Cut Pro X helps small company delight world’s biggest clients</title>
            <link>http://www.apple.com/final-cut-pro/in-action/trim-editing/?sr=hotnews.rss</link>
            <description>When Trim Editing started creating music videos over a decade ago, just paying the rent was a huge accomplishment. Now, the small East London company is crafting award-winning visuals for big brands — like Audi, Nike, Adidas, and Guinness — propelled by the power of Final Cut Pro X. The video editing software’s comprehensive features allow Trim Editing to organize film and audio clips, pull together compelling projects, and make changes on the fly. “When I’m playing back an edit for a director, they’ll say, ‘Okay, let’s go and make those changes I talked about.’ I’ll say, ‘Oh, no, they’re already done,’ and we’ll jump back and watch it again. People can’t believe that I’ve magically done the change before we even finish playback,” says editor Thomas Grove Carter. </description>
            <pubDate>Wed, 20 Apr 2016 10:05:59 PDT</pubDate>
        </item>
        <item>
            <title>Apple Introduces 9.7-inch iPad Pro</title>
            <link>http://www.apple.com/ipad-pro/?sr=hotnews.rss</link>
            <description>Apple today introduced the 9.7-inch iPad Pro, which at just under one pound features a new pro Retina display with greater brightness, wider color gamut, lower reflectivity, Night Shift mode, and new True Tone display technology. The new iPad Pro also has a 64-bit A9X chip that rivals most portable PCs. “iPad Pro is a new generation of iPad that is indispensable and immersive, enabling people to be more productive and more creative. It’s incredibly fast, extremely portable, and completely natural to use with your fingers, Apple Pencil, and Smart Keyboard. And now it comes in two sizes,” said Philip Schiller, Apple’s senior vice president of Worldwide Marketing.</description>
            <pubDate>Mon, 21 Mar 2016 12:00:03 PDT</pubDate>
        </item>
    </channel>
</rss>

You need to create the model classes like:

class RSSFeed: XMLMappable {
    var nodeName: String!

    var channel: Channel?

    required init?(map: XMLMap) {}

    func mapping(map: XMLMap) {
        channel <- map["channel"]
    }
}

class Channel: XMLMappable {
    var nodeName: String!

    var title: String?
    var link: URL?
    var description: String?
    var language: String?
    var copyright: String?
    var pubDate: Date?
    var lastBuildDate: Date?
    var category: String?
    var generator: String?
    var docs: URL?
    var items: [Item]?

    private static var dateFormatter: DateFormatter = {
        var dateFormatter = DateFormatter()
        dateFormatter,dateFormat = "E, d MMM yyyy HH:mm:ss zzz"
        return dateFormatter
    }()

    required init?(map: XMLMap) {}

    func mapping(map: XMLMap) {
        title <- map["title"]
        link <- (map["link"], XMLURLTransform())
        description <- map["description"]
        language <- map["language"]
        copyright <- map["copyright"]
        pubDate <- (map["pubDate"], XMLDateFormatterTransform(dateFormatter: Channel.dateFormatter))
        lastBuildDate <- (map["lastBuildDate"], XMLDateFormatterTransform(dateFormatter: Channel.dateFormatter))
        category <- map["category"]
        generator <- map["generator"]
        docs <- (map["docs"], XMLURLTransform())
        items <- map["item"]
    }
}

class Item: XMLMappable {
    var nodeName: String!

    var title: String?
    var link: URL?
    var description: String?
    var pubDate: Date?

    private static var dateFormatter: DateFormatter = {
        var dateFormatter = DateFormatter()
        dateFormatter,dateFormat = "E, d MMM yyyy HH:mm:ss zzz"
        return dateFormatter
    }()

    required init?(map: XMLMap) {}

    func mapping(map: XMLMap) {
        title <- map["title"]
        link <- (map["link"], XMLURLTransform())
        description <- map["description"]
        pubDate <- (map["pubDate"], XMLDateFormatterTransform(dateFormatter: Item.dateFormatter))
    }
}

Using the native URLSession you can map the RSS XML response using XMLSerialization and XMLMapper:

let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
    do {
        let xmlDictionary = try XMLSerialization.xmlObject(with: data!) as? [String: Any]
        let rssFeed = XMLMapper<RSSFeed>().map(XMLObject: xmlDictionary)

        print(rssFeed?.items?.first?.title ?? "nil")
    } catch {
        print("Serialization error occurred: \(error.localizedDescription)")
    }
}
task.resume()

If you don't mind using Alamofire for the request, you will find XMLMapper/Requests subspec a lot easier using this code to map:

Alamofire.request(url).responseXMLObject { (response: DataResponse<RSSFeed>) in
    let rssFeed = response.result.value
    print(rssFeed?.items?.first?.title ?? "nil")
}

I hope this is helpful.

Tags:

Xml

Parsing

Swift