Episode #401

Adding Episode Cells

Series: Making a Podcast App From Scratch

27 minutes
Published on July 19, 2019

This video is only available to subscribers. Get access to this video and 584 others.

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)
}     

This episode uses Xcode 10.2.1, Swift 5.0.