
This video is only available to subscribers. Start a subscription today to get access to this and 469 other videos.
Parsing RSS and Atom Feeds
This episode is part of a series: Making a Podcast App From Scratch.
Episode Links
Parsing RSS and Atom Feeds
To get the information we need for the Podcast Detail screen, we’ll have to get the feed URL and parse it. There’s no built-in Codable support for XML, so we’ll look at using FeedKit to parse the feeds and extract the relevant information we need.
Installing FeedKit
We will start by adding FeedKit to the Podfile
.
pod 'FeedKit'
We need to install FeedKit
pod install
Creating a model to represent a Podcast
To store the extracted data we will create a basic model Podcast
with optional settings.
class Podcast {
var title: String?
var author: String?
var description: String?
var primaryGenre: String?
var artworkURL: URL?
}
Create an enum to define loading errors arising from FeedKit and network issues.
enum PodcastLoadingError : Error {
case networkingError(Error)
case requestFailed(Int)
case serverError(Int)
case notFound
case feedParsingError(Error)
case missingAttribute(String)
}
We will define a function that will take a feed and call a completion block later with the loaded Podcast
instance
import Foundation
import FeedKit
class PodcastFeedLoader {
func fetch(feed: URL, completion: @escaping (Swift.Result<Podcast, PodcastLoadingError>) -> Void) {
//...
}
}
We will first fetch the data by creating the request to URLRequest
. As data may not change frequently, we will use returnCacheDataElseLoad
cache policy which will return cached data or we will load from the original source.
let req = URLRequest(url: feed, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 60)
We will use URLSession
to create a network request using URLRequest
and completionHandler
. Handling the errors as a networking error.
URLSession.shared.dataTask(with: req) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(.networkingError(error)))
}
return
}
}
In the case of response, check the HTTP.statusCode
. For status code 200
we will parse the data using FeedKitset while for failure case, use PodcastLoadingError
to define failure cases.
let http = response as! HTTPURLResponse
switch http.statusCode {
case 200:
if let data = data {
self.loadFeed(data: data, completion: completion)
}
case 404:
DispatchQueue.main.async {
completion(.failure(.notFound))
}
case 500...599:
DispatchQueue.main.async {
completion(.failure(.serverError(http.statusCode)))
}
default:
DispatchQueue.main.async {
completion(.failure(.requestFailed(http.statusCode)))
}
}
Don't forget to resume
dataTask.
Once we get data, we will use FeedParser
to parse the feed asynchronously. We are extracting information obtained from Atom
and RSS
feed format. In this tutorial, we are not handing feed format obtained in JSON
.
private func loadFeed(data: Data, completion: @escaping (Swift.Result<Podcast, PodcastLoadingError>) -> Void) {
let parser = FeedParser(data: data)
parser.parseAsync { parseResult in
let result: Swift.Result<Podcast, PodcastLoadingError>
do {
switch parseResult {
case .atom(let atom):
result = try .success(self.convert(atom: atom))
case .rss(let rss):
result = try .success(self.convert(rss: rss))
case .json(_): fatalError()
case .failure(let e):
result = .failure(.feedParsingError(e))
}
} catch let e as PodcastLoadingError {
result = .failure(e)
} catch {
fatalError()
}
DispatchQueue.main.async {
completion(result)
}
}
}
Now, we will have a check for required and optional attribute in the parsed feed.
Note the attributes in Atom
feed format. Check for subtitle
and author
attribute. Extract the relevant information and create a Podcast
object.
private func convert(atom: AtomFeed) throws -> Podcast {
guard let name = atom.title else { throw PodcastLoadingError.missingAttribute("title") }
let author = atom.authors?.compactMap({ $0.name }).joined(separator: ", ") ?? ""
guard let logoURL = atom.logo.flatMap(URL.init) else {
throw PodcastLoadingError.missingAttribute("logo")
}
let description = atom.subtitle?.value ?? ""
let p = Podcast()
p.title = name
p.author = author
p.artworkURL = logoURL
p.description = description
return p
}
Note that RSS feed has description
attribute and iTunesOwner
. Store this information in a Podcast
object.
private func convert(rss: RSSFeed) throws -> Podcast {
guard let title = rss.title else { throw PodcastLoadingError.missingAttribute("title") }
guard let author = rss.iTunes?.iTunesOwner?.name else {
throw PodcastLoadingError.missingAttribute("itunes:owner name")
}
let description = rss.description ?? ""
guard let logoURL = rss.iTunes?.iTunesImage?.attributes?.href.flatMap(URL.init) else {
throw PodcastLoadingError.missingAttribute("itunes:image url")
}
let p = Podcast()
p.title = title
p.author = author
p.artworkURL = logoURL
p.description = description
return p
}
Resolving Security Issue
Next let's create a test for PodcastFeedLoader
by creating an array of feed URLs. We will run into issues loading arbitrary feeds because not all of them work over HTTPS. To allow these to load we need to disable App Transport Security.
Open up Info.plist
and create an entry for App Transport Security Settings, then set its Allow Arbitrary Loads to YES.