Parse XML and RSS feeds in iOS using XMLParser with the XMLParserDelegate protocol. This guide builds a complete RSS reader with expandable cells.
How XMLParser Works in iOS
XMLParser uses SAX parsing. It reads XML documents sequentially and fires delegate events for each element. This approach uses less memory than DOM parsing because it never loads the entire document as a tree.
The XMLParserDelegate protocol provides these key methods:
parserDidStartDocument(_:)- called when parsing beginsparserDidEndDocument(_:)- called when parsing finishesparser(_:didStartElement:namespaceURI:qualifiedName:attributes:)- called for each opening tagparser(_:didEndElement:namespaceURI:qualifiedName:)- called for each closing tagparser(_:foundCharacters:)- called for text content between tagsparser(_:parseErrorOccurred:)- called on parsing errors
Building the FeedParser Class
Create a FeedParser class that adopts XMLParserDelegate. Use a tuple type for RSS items:
typealias RSSItem = (title: String, description: String, pubDate: String)
class FeedParser: NSObject, XMLParserDelegate {
private var rssItems: [RSSItem] = []
private var currentElement = ""
private var currentTitle = "" {
didSet { currentTitle = currentTitle.trimmingCharacters(in: .whitespacesAndNewlines) }
}
private var currentDescription = "" {
didSet { currentDescription = currentDescription.trimmingCharacters(in: .whitespacesAndNewlines) }
}
private var currentPubDate = "" {
didSet { currentPubDate = currentPubDate.trimmingCharacters(in: .whitespacesAndNewlines) }
}
private var completionHandler: (([RSSItem]) -> Void)?
}
Define an RSSFeedTag enum for the XML elements you parse:
enum RSSFeedTag: String {
case item = "item"
case title = "title"
case description = "description"
case pubDate = "pubDate"
}
Fetching RSS Data Asynchronously
Use URLSession to download RSS feed data asynchronously. XMLParser parses data synchronously, so pass the downloaded Data to the parser:
func parseFeed(feedURL: String, completion: (([RSSItem]) -> Void)?) {
self.completionHandler = completion
guard let url = URL(string: feedURL) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
if let error = error { print(error.localizedDescription) }
return
}
let parser = XMLParser(data: data)
parser.delegate = self
parser.parse()
}
task.resume()
}
Implementing XMLParserDelegate Methods
Reset the items array when parsing starts:
func parserDidStartDocument(_ parser: XMLParser) {
rssItems = []
}
Track the current element and reset temporary variables when entering an :
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) {
currentElement = elementName
if elementName == RSSFeedTag.item.rawValue {
currentTitle = ""
currentDescription = ""
currentPubDate = ""
}
}
Append parsed characters to the appropriate variable:
func parser(_ parser: XMLParser, foundCharacters string: String) {
switch currentElement {
case RSSFeedTag.title.rawValue: currentTitle += string
case RSSFeedTag.description.rawValue: currentDescription += string
case RSSFeedTag.pubDate.rawValue: currentPubDate += string
default: break
}
}
Create an RSS item when each closes:
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
if elementName == RSSFeedTag.item.rawValue {
let item = (title: currentTitle, description: currentDescription, pubDate: currentPubDate)
rssItems.append(item)
}
}
Call the completion handler when parsing finishes:
func parserDidEndDocument(_ parser: XMLParser) {
completionHandler?(rssItems)
}
Using FeedParser in a View Controller
Call the parser and reload the table view on the main thread:
let parser = FeedParser()
parser.parseFeed(feedURL: "https://www.nasa.gov/rss/dyn/breaking_news.rss") { items in
self.rssItems = items
OperationQueue.main.addOperation {
self.tableView.reloadData()
}
}
Implementing Expandable and Collapsible Cells
Limit the description label to 4 lines for the collapsed state:
@IBOutlet weak var descriptionLabel: UILabel! {
didSet {
descriptionLabel.numberOfLines = 4
}
}
Toggle between collapsed and expanded when the user taps a cell. Wrap changes in beginUpdates() and endUpdates() for smooth animation:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let cell = tableView.cellForRow(at: indexPath) as! RSSNewsCell
tableView.beginUpdates()
cell.descriptionLabel.numberOfLines = cell.descriptionLabel.numberOfLines == 0 ? 4 : 0
tableView.endUpdates()
}
Preventing the Cell Reuse Bug
TableView cells reuse their views. When a cell scrolls off screen and returns, its visual properties reset. Track the expandable state in a separate array:
enum CellState {
case expanded
case collapsed
}
private var cellStates: [CellState]?
Initialize the array after fetching RSS items:
self.cellStates = [CellState](repeating: .collapsed, count: items.count)
Update state when the user taps a cell:
cellStates?[indexPath.row] = cell.descriptionLabel.numberOfLines == 0 ? .expanded : .collapsed
Set the label lines based on stored state in cellForRowAt:
if let states = cellStates {
cell.descriptionLabel.numberOfLines = states[indexPath.row] == .expanded ? 0 : 4
}
Best Practices for XML Parsing
Trim whitespace from parsed values. RSS feeds contain irregular whitespace and newlines. Apply trimmingCharacters(in: .whitespacesAndNewlines) in property observers.
Handle errors in parser(_:parseErrorOccurred:). Malformed XML, network failures, and unexpected element structures require graceful handling. Log errors and show user-friendly messages.
Use async networking with URLSession. Never call XMLParser with synchronous URL loading on the main thread. The delegate methods run on the calling thread.
Best Practices for Expandable Cells
Track cell state in the data model, not the view. Cell reuse corrupts view-level state. Maintain a parallel array or include state in your data model.
Animate height changes with beginUpdates() and endUpdates(). This produces smooth expand and collapse animations. Without these calls, the cell height jumps instantly.
Limit preview text to 3-4 lines. Short previews give users enough context to decide whether to expand without overwhelming the list.
Summary
This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.
Frequently Asked Questions
What is XMLParser? XMLParser is a Foundation class that reads XML one element at a time and reports what it finds to its delegate through the XMLParserDelegate protocol.
What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.