Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parsing XML from URL in Swift

Tags:

parsing

xml

swift

I am brand new to parsing and cannot find any tutorial that isn't outdated and doesn't raise more questions. I have a simple xml file url I am trying to parse. The xml is very simple:

<xml>
    <record>
        <EmpName>A Employee</EmpName>
        <EmpPhone>111-222-3333</EmpPhone>
        <EmpEmail>[email protected]</EmpEmail>
        <EmpAddress>12345 Fake Street</EmpAddress>
        <EmpAddress1>MyTown, Mystate ZIP</EmpAddress1>
    </record>
</xml>

And just wanted to save this as an NSDictionary (tags as keys and data as values). So far all I have been able to do successfully is print the xml string in the console with:

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

let task = NSURLSession.sharedSession().dataTaskWithURL(url!) {(data, response, error) in
        println(NSString(data: data, encoding: NSUTF8StringEncoding))
}
print(task)
task.resume()

I have been through any online tutorials that I've found and are either outdated or much too complicated. Any help is appreciated.

like image 929
Tamarisk Avatar asked Jun 26 '15 23:06

Tamarisk


People also ask

What is XML file in Swift?

If you are not familiar with XML, it's basically a precisely formatted text or string, which can be parsed into an array of objects containing the precious information. A good tutorial about XML can be found here.

Why use XML?

By using XML, Web agents and robots (programs that automate Web searches or other tasks) are more efficient and produce more useful results. General applications: XML provides a standard method to access information, making it easier for applications and devices of all kinds to use, store, transmit, and display data.


2 Answers

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.

like image 131
Rob Avatar answered Oct 08 '22 15:10

Rob


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.

like image 24
gcharita Avatar answered Oct 08 '22 17:10

gcharita