How to Parse RSS Feeds and XML Data in iOS

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 begins
  • parserDidEndDocument(_:) - called when parsing finishes
  • parser(_:didStartElement:namespaceURI:qualifiedName:attributes:) - called for each opening tag
  • parser(_:didEndElement:namespaceURI:qualifiedName:) - called for each closing tag
  • parser(_:foundCharacters:) - called for text content between tags
  • parser(_: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.

All articles