In this episode we extract episode information from the podcast feed and render them as cells on the podcast detail screen.
Creating Custom Episode Cell We will start by creating a class EpisodeCell with properties to display the title, information, and description of the podcast. We will customize this cell design by setting the background color and text color. class EpisodeCell: UITableViewCell { @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var infoLabel: UILabel! @IBOutlet weak var descriptionLabel: UILabel! override func awakeFromNib() { super.awakeFromNib() backgroundColor = Theme.Colors.gray5 contentView.backgroundColor = Theme.Colors.gray5 titleLabel.textColor = Theme.Colors.gray0 infoLabel.textColor = Theme.Colors.gray2 descriptionLabel.textColor = Theme.Colors.gray2 } } Adding Data Source We will override a few functions to define the number of rows and to extract the cell data for the selected episode. To reuse the cells in the table view we will use .dequeueReusableCell, this will identify the cell defined on the table view with the name EpisodeCell. We will use some sample data initially, but later we will replace this with the formatted feed data. override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return podcast.episodes.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: EpisodeCell = tableView.dequeueReusableCell(for: indexPath) cell.titleLabel.text = "Episode title" cell.infoLabel.text = "01:42::28 • May 3rd" cell.descriptionLabel.text = "This is a description of the episode and it is quite long" return cell } In the storyboard we will add labels in a stack view and set the hugging priority. This will allow the description label to grow if we have enough space. Populating The Feed Data Into Episode Model To populate the cell with real data we will create a model and extract all the information from the feed to this model. We will have an array of episode initialized with an empty array. class Podcast { var title: String? var author: String? var description: String? var primaryGenre: String? var artworkURL: URL? var episodes: [Episode] = [] } Next we will create a class for our episode, with an optional identifier. We'll need these values to uniquely identify an episode. We'll also capture the enclosureURL, which will be used to download and play the audio. class Episode { var identifier: String? var title: String? var description: String? var publicationDate: Date? var duration: TimeInterval? var enclosureURL: URL? } Extracting details from the Feed Next we will extract the details from the Atom and RSS feeds by mapping the feed details to the episode. In an Atom feed, we can iterate over the entries and extract the data we need: p.episodes = (atom.entries ?? []).map { entry in let episode = Episode() episode.identifier = entry.id episode.title = entry.title episode.description = entry.summary?.value episode.enclosureURL = entry.content?.value.flatMap(URL.init) return episode } Similarly, we can map over the items of an RSS feed: p.episodes = (rss.items ?? []).map { item in let episode = Episode() episode.identifier = item.guid?.value episode.title = item.title episode.description = item.description episode.publicationDate = item.pubDate episode.duration = item.iTunes?.iTunesDuration episode.enclosureURL = item.enclosure?.attributes?.url.flatMap(URL.init) return episode } Creating View Model To Format Feed Data We will have to format this data before we can display it. This is a good use of a view model. While formating the data for infoLabel of a cell we will obtain a non-nil data array of timeString and dateString joined by separator •. As we need to format the date and time data, we will create a static formatter using the style of formatting defined for each formatter. This will avoid creating numerous redundant formatters. struct EpisodeCellViewModel { private let episode: Episode private static var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .short return formatter }() private static var timeFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.unitsStyle = .short formatter.allowedUnits = [.hour, .minute, .second] return formatter }() init(episode: Episode) { self.episode = episode } var title: String { return episode.title ?? "<untitled>" } var description: String? { return episode.description } var info: String { let parts = [timeString, dateString].compactMap { $0 } return parts.joined(separator: " • ") } private var timeString: String? { guard let duration = episode.duration else { return nil } return EpisodeCellViewModel.timeFormatter.string(from: duration) } private var dateString: String? { guard let publicationDate = episode.publicationDate else { return nil } return EpisodeCellViewModel.dateFormatter.string(from: publicationDate) } } Rendering The Formatted Data in Podcast Detail Screen. To display the formatted data in the podcast detail screen we will configure the EpisodeCell to the title, info, and description labels. func configure(with viewModel: EpisodeCellViewModel) { titleLabel.text = viewModel.title infoLabel.text = viewModel.info descriptionLabel.text = viewModel.description } We will then replace our dummy data with formatted data for each episode. While the table view is loading, we will not have podcast episodes loaded, to handle this we will return 0 cells and also check the podcast before assigning the data to view model. if let episode = podcast?.episodes[indexPath.row] { let viewModel = EpisodeCellViewModel(episode: episode) cell.configure(with: viewModel) }