Arjuna Sky Kok I'm a software engineer, a Youtuber, and a writer. I build PredictSalary, SailorCoin, and Pembangun.

XML parsing in Swift: Tutorial with examples

7 min read 2190

XML Parsing In Swift: Tutorial With Examples

Although the XML file format sounds outdated compared to the JSON file format, XML is very much alive. JSON might be more popular in web development, but XML has many features that JSON lacks, such as namespace support that is useful to avoid name collision for elements and attributes. XML also has a schema that can force an XML document to adhere to a certain standard.

You don’t need to look for a third-party library to manipulate an XML file with Swift. The Swift standard library has XMLParser for all your XML needs.

Jump ahead:

Setting up XMLParser and XMLParserDelegate

Create a Playground project in XCode and name it XMLParsingPlayground. Refer to this article to learn more about this step.

To parse the XML data, you need to convert an XML string to Data. Add the following code:

let xmlContent =
"""
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <article>
        <title>Getting Started with Swift</title>
    </article>
    <article>
        <title>How to Parse XML with Rust</title>
    </article>
</root>
""";

let xmlData = Data(xmlContent.utf8)

In the code above, you initialized a Data instance with an XML string. Now, you need to initialize an XMLParser instance with this Data instance:

let xmlParser = XMLParser(data: xmlData)

XMLParser will delegate the parsing logic to its delegation class. You’ll need to implement this class that inherits NSObject and XMLParserDelegate. Add the following code to create the class and its instance:

class Parser1 : NSObject, XMLParserDelegate {

    func parserDidStartDocument(_ parser: XMLParser) {
        print("Start of the document")
        print("Line number: \(parser.lineNumber)")
    }

    func parserDidEndDocument(_ parser: XMLParser) {
        print("End of the document")
        print("Line number: \(parser.lineNumber)")
    }

}
let parser1 = Parser1()

In the delegated class, you set up a logic to do something when you hit the start of the document and the end of the document in the parsing process. If you encounter these, you will print some strings and a line number where the parser is right now. Both methods accept an XMLParser instance in the arguments. Many methods in the delegated class accept an XMLParser instance in their arguments.

Once you get the parser delegation instance done, you have to set it as the XMLParser instance’s delegation class:

xmlParser.delegate = parser1

The last step is to parse it:

xmlParser.parse()

If you compiled and ran the program, you should get the following output:

Start of the document
Line number: 1
End of the document
Line number: 9

The code above means that when the parser met the event of the start of the document, it was in line 1. Then it went to the content of the document. It traveled through the elements but because you didn’t implement methods to handle this, nothing happened. Finally, when it hit the event of the end of the document, it informed us through the parserDidEndDocument method that you’ve implemented.

Handling elements of the XML data

When parsing XML, you need to implement methods to handle the nodes or events that you’re interested in. If you’re interested in comments, you need to implement a method when the parser meets comments in the XML data.

Now, you want to implement methods for when a parser meets elements like article and title. The method that you need to implement is the parser method with the following signature:

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

Create a new parser delegated class:

class Parser2 : NSObject, XMLParserDelegate {

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

}

In the new delegated class, you didn’t implement parserDidStartDocument and parserDidEndDocument because you’re only focused on parsing elements.

As usual, you need to set the delegated class instance of a new XMLParser instance and call the parse method again:

let parser2 = Parser2()
let xmlParser2 = XMLParser(data: xmlData)
xmlParser2.delegate = parser2

xmlParser2.parse()

Compile and run the program, and it will print the elements’ names:

root
article
title
article
title

As you can see, there are five elements. If you want to differentiate title elements because there are two of them, you need to add more logic. One of the examples is to track how many article elements the parse has met.

Create another delegated class:

class Parser3 : NSObject, XMLParserDelegate {

    var articleNth = 0

    func parser(
        _ parser: XMLParser,
        didStartElement elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
        attributes attributeDict: [String : String] = [:]
    ) {
        if (elementName=="article") {
            articleNth += 1
        } else if (elementName=="title") {
            print("'\(elementName)' in the article element number \(articleNth)")
        }
    }

}

This time, you tracked how many article elements you’ve met so when you meet the title element, you know which article element you are in.

Create a new XMLParser instance and set the delegated class to Parser3:

let parser3 = Parser3()
let xmlParser3 = XMLParser(data: xmlData)
xmlParser3.delegate = parser3

xmlParser3.parse()

If you compile and run the program, you should see this output:

'title' in the article element number 1
'title' in the article element number 2

Handling the attributes of elements of the XML data

Elements in the XML data can have attributes as well. You might want to query the attributes. The recent method that you’ve executed has another argument that refers to the attributes. But first, you have to create other XML data because the data right now does not have the attributes:

let xmlContent_attributes =
"""
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <article id="1" tag="swift">
        <title>Getting Started with Swift</title>
    </article>
    <article id="2" tag="rust">
        <title>How to Parse XML with Rust</title>
    </article>
</root>
""";
let xmlData_attributes = Data(xmlContent_attributes.utf8)

This time, you added attributes on the article elements. The attributes are id and tag.

Now, you have to create the delegated class to handle the attributes:

class Parser4 : NSObject, XMLParserDelegate {

    func parser(
        _ parser: XMLParser,
        didStartElement elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
        attributes attributeDict: [String : String] = [:]
    ) {
        for (attr_key, attr_val) in attributeDict {
            print("Key: \(attr_key), value: \(attr_val)")
        }
    }

}

The method is the same as the method you previously implemented. But this time, you didn’t ignore the attributeDict argument.



We’re not finished yet! Now, you have to create a new XMLParser instance that uses this delegated class instance:

let parser4 = Parser4()
let xmlParser4 = XMLParser(data: xmlData_attributes)
xmlParser4.delegate = parser4

xmlParser4.parse()

If you compiled and ran the program, you would see the attributes:

Key: id, value: 1
Key: tag, value: swift
Key: id, value: 2
Key: tag, value: rust

Handling the namespace in the XML data

Now, there are two arguments you haven’t dealt with in the parser method. They are namespaceURI and qName, which are related to each other.

First, you need to add namespaces into the XML data:

let xmlContent_namespace =
"""
<?xml version="1.0" encoding="UTF-8"?>
<root xmlns:t="http://logrocket.com/tech" xmlns:m="http://logrocket.com/marketing">
    <t:article>
        <t:title>Getting Started with Swift</t:title>
    </t:article>
    <m:article>
        <m:title>How to Parse XML with Rust</m:title>
    </m:article>
</root>
""";

let xmlData_namespace = Data(xmlContent_namespace.utf8)

There are two namespaces in this XML data: http://logrocket.com/tech and http://logrocket.com/marketing. Elements inside the root element use namespaces. It’s not article anymore, but t:article or m:article.

The t refers to xmlns:t, which has the value http://logrocket.com/tech. The m refers to xmlns:m, which has the value http://logrocket.com/marketing.

Then you need to create the delegated class to handle the namespace:

class Parser5 : NSObject, XMLParserDelegate {

    func parser(
        _ parser: XMLParser,
        didStartElement elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
        attributes attributeDict: [String : String] = [:]
    ) {
        print("Namespace URI: \(namespaceURI!), qualified name: \(qName!)")
    }

}

To get the namespace URI, for example, http://logrocket.com/tech, you can use the namespaceURI argument. To get the qualified name, for example, t:article, you can use the qName argument.

The next step is important. Create a new XMLParser instance to handle the namespace:

let parser5 = Parser5()
let xmlParser5 = XMLParser(data: xmlData_namespace)
xmlParser5.delegate = parser5
xmlParser5.shouldProcessNamespaces = true

xmlParser5.parse()

Before you compile and run the program, notice there is a line that tells the parser to process the namespace. To enable the namespace in the XML data, you need to set the shouldProcessNamespaces property of the parser to true. Otherwise, the namespaces will be ignored.

Compile and run the program. Then you will get the namespace URIs and the qualified names:

Namespace URI: , qualified name: root
Namespace URI: http://logrocket.com/tech, qualified name: t:article
Namespace URI: http://logrocket.com/tech, qualified name: t:title
Namespace URI: http://logrocket.com/marketing, qualified name: m:article
Namespace URI: http://logrocket.com/marketing, qualified name: m:title

Getting the text content when parsing the XML data

Now, it’s time to get the text content from the XML data. The method that you need to implement is the parser method with the following signature:

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

Add the following XML data:

let xmlContent_text =
"""
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <article>
        <title>Getting Started with Swift</title>
        <published>true</published>
    </article>
    <article>
        <title>How to Parse XML with Rust</title>
        <published>false</published>
    </article>
</root>
""";
let xmlData_text = Data(xmlContent_text.utf8)

Then you need to implement the delegated class that has a method to handle the text content:

class Parser6 : NSObject, XMLParserDelegate {

    func parser(
        _ parser: XMLParser,
        foundCharacters string: String
    ) {
        if (string.trimmingCharacters(in: .whitespacesAndNewlines) != "") {
            print(string)
        }
    }

}

You’re only interested in the text content that has some characters. If you didn’t implement the filter, it would result in a lot of blank text content. The text context is defined as the text between element nodes. That includes the blank text between the article element and the title element as well:

    <article>
        <title>How to Parse XML with Rust</title>

It’s not just white spaces, but there is a new line between article and title. That’s why you trimmed it first and then checked whether it was an empty string or not.

Finally, create an XMLParser instance to handle the text content:

let parser6 = Parser6()
let xmlParser6 = XMLParser(data: xmlData_text)
xmlParser6.delegate = parser6

xmlParser6.parse()

If you compiled and ran the program, you should get this text content:

Getting Started with Swift
true
How to Parse XML with Rust
false

Handling errors when parsing the XML data

Life is not ideal. Sometimes you get imperfect XML data and the data is corrupted. But don’t worry! You can handle errors with the parser method with the following signature:

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

But first, you need to write the corrupted XML data:

let xmlContent_corrupted =
"""
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <article>
        <title>Getting Started with Swift</title>
    </article>
    <article>
        <title>How to Parse XML with Rust
    </article>
</root>
""";
let xmlData_corrupted = Data(xmlContent_corrupted.utf8)

Notice that there is a missing closing title tag.

Then, create a delegated class to handle the error:

class Parser7 : NSObject, XMLParserDelegate {

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

}

This method gave the parseError argument that has the information about the error. In this case, you printed the error itself.

What does the error look like? You need to create the XMLParser instance to take this delegated class:

let parser7 = Parser7()
let xmlParser7 = XMLParser(data: xmlData_corrupted)
xmlParser7.delegate = parser7

xmlParser7.parse()

If you compiled and ran the program, you should see the error:

Error Domain=NSXMLParserErrorDomain Code=76 "(null)" UserInfo={NSXMLParserErrorColumn=15, NSXMLParserErrorLineNumber=8, NSXMLParserErrorMessage=Opening and ending tag mismatch: title line 7 and article
}

You got the line number and column number where the error happened. You also received the error message, which told you that the opening and ending tags were mismatched.

Conclusion

In this article, you learned how to parse XML data. You started by creating the XMLParser instance. Then you created a delegated XMLParserDelegate class to handle the logic of the parsing process. From there, you handled elements, attributes, and text content. You also managed an error in the parsing process and configured the parser to handle the namespace.


More great articles from LogRocket:


This article only scratches the surface of XML parsing in Swift. You can learn more about the XML parsing API in the documentation here. The code for this article is available on this GitHub repository.

Arjuna Sky Kok I'm a software engineer, a Youtuber, and a writer. I build PredictSalary, SailorCoin, and Pembangun.

Leave a Reply