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
andXMLParserDelegate
- Handling elements of the XML data
- Handling the attributes of elements of the XML data
- Handling the namespace in the XML data
- Getting the text content when parsing the XML data
- Handling errors when parsing the XML data
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.
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.
The post XML parsing in Swift: Tutorial with examples appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/tovF0AY
Gain $200 in a week
via Read more