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.
Episode Links iTunes RSS Feed Generator iTunes Search API FeedKit 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.