Episode #404

Player Screen

Series: Making a Podcast App From Scratch

27 minutes
Published on August 8, 2019

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

We've spent a lot of time dealing with the data, networking, architecture, and overall theme of our podcast app, but we haven't yet written a player! So in this episode we start the process of designing our player screen. We'll start by adding all of the controls and views to our PlayerViewController, wire everything up, and customize the look & feel to match our Sketch design.

Designing the Player

We will start by designing our player screen as per the sketch file. We will create a storyboard for our player.

1) We will add a UIButton to dismiss the view. We will set the leading space and top space defined to at the top left edge of the view. By setting the type to custom we will set a default image instead of button title and also set the image size bit bigger and constraint its weight and height to make it easily tappable.

2) Next we will add the title label to the center of the screen, with top spacing and horizontal spacing constraint along with the dismiss button. For longer titles, we will wrap it in two lines by setting the location to the center and line property set to 2.

3) Next we will add a square-shaped image view, and center it horizontally will a vertical space set from the label. To add a shadow we will embed the view without the inset. We will next set our shadow wrapper to separate vertically to the label and horizontally centered. Also, we will set the image view constraint to all sides of its parent.

4) We will add the slider spanning the entire width of the view. We will set the slider with vertical spacing and the minimum and maximum values to 0 and 1. As we need the slider events triggered during its dragging we will select the Continuous Updates for the Events of the slider.

5) Now we will add two labels to the leading and trailing edge of the slider and set its vertical spacing constraint. This will display the progress and remaining time of a podcast we are playing.

6) Finally, we will add three buttons with the default image set to skip-back, play, and skip-forward. While the play button is selected we will set the image to pause, this is done by selecting the selected state for State Config of the play button and then set the image to pause icon. We will also set the spacing vertically and center horizontally constraint to play button.

Creating the Player View Controller

We will start by adding all the controls and actions to our PlayerViewController. We will create a static instance of our controller this will allow us to view our episode from the playlist, libraries and even from the detail screen. Now we will add the action to dismiss the episode and its view load methods.

class PlayerViewController : UIViewController {

    static var shared: PlayerViewController = {
        let storyboard = UIStoryboard(name: "Player", bundle: nil)
        let playerVC = storyboard.instantiateInitialViewController() as! PlayerViewController
        _ = playerVC.view
        return playerVC
    }()

    // MARK: - Outlets
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var artworkImageView: UIImageView!
    @IBOutlet weak var artworkShadowWrapper: UIView!
    @IBOutlet weak var transportSlider: UISlider!
    @IBOutlet weak var timeProgressedLabel: UILabel!
    @IBOutlet weak var timeRemainingLabel: UILabel!
    @IBOutlet weak var playPauseButton: UIButton!


    override func viewDidLoad() {
         super.viewDidLoad()
    }

    // MARK: - Actions
    @IBAction func dismissTapped(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }

    @IBAction func transportSliderChanged(_ sender: Any) {
    }

    @IBAction func skipBack(_ sender: Any) {
    }

    @IBAction func skipForward(_ sender: Any) {
    }

    @IBAction func playPause(_ sender: Any) {
    }
}

Loading the UI With Episode Details

While we update the UI we will set the episode and podcast in lockstep from the PodcastDetailViewController.

// MARK: - UITableViewDelegate

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) 
{
    guard let podcast = podcast else { return }
    let episode = podcast.episodes[indexPath.row]

    let player = PlayerViewController.shared
    player.setEpisode(episode, podcast: podcast)
    present(player, animated: true, completion: nil)
}

Next, we will create a function to set the episode title details and image in our PlayerViewController. Make sure to force load the view while creating the instance of the player. We will import the Kingfisher to set the artwork image.

func setEpisode(_ episode: Episode, podcast: Podcast) {
    titleLabel.text = episode.title
    artworkImageView.kf.setImage(with: podcast.artworkURL, options: [.transition(.fade(0.3))])
}

Theme Setting

Next, we will set the theme for background, text, and style to our status bar.

view.backgroundColor = Theme.Colors.gray4

titleLabel.textColor = Theme.Colors.gray1
timeRemainingLabel.textColor = Theme.Colors.gray2
timeProgressedLabel.textColor = Theme.Colors.gray2

timeRemainingLabel.text = nil
timeProgressedLabel.text = nil

As we have set a dark background to our view, we will set the preferredStatusBarStyle to lighter content.

override var preferredStatusBarStyle: UIStatusBarStyle {
    return .lightContent
}

Next, we will a few properties to our artwork image. To display the rounded corners we will set the .maskToBound and .cornerRadius properties of the image along with the properties of artworkShadowWrapper to display a deep shadow along with the image. We will set the background color of artwork similar to the UI, in case if there is no artwork the background color will be displayed, also we will clear the background color of shadowWrapper else it will display a white color.

artworkImageView.layer.masksToBounds = true
artworkImageView.layer.cornerRadius = 12
artworkImageView.backgroundColor = Theme.Colors.gray4

artworkShadowWrapper.backgroundColor = .clear
artworkShadowWrapper.layer.shadowColor = UIColor.black.cgColor
artworkShadowWrapper.layer.shadowOpacity = 0.9
artworkShadowWrapper.layer.shadowOffset = CGSize(width: 0, height: 1)
artworkShadowWrapper.layer.shadowRadius = 20

Customizing the Slider and Play Button

We will now set the toggle function to play button while it is in the selected state. We will also set the fixed image size for pause and play buttons.

@IBAction func playPause(_ sender: Any) {
    playPauseButton.isSelected.toggle()
}

To dim the image using core graphics we will add purpleDimmed color to our Theme file.

static var purpleDimmed = UIColor(red: 0.30, green: 0.04, blue: 0.53, alpha: 1.00)

Now we will set this purpleDimmed image to play and pause button in selected and highlighted state.

let pauseImage = playPauseButton.image(for: .selected)
let tintedImage = pauseImage?.tint(color: Theme.Colors.purpleDimmed)
playPauseButton.setImage(tintedImage, for: [.selected, .highlighted])

Finally, will set a knob for the slider. While the slider is in highlighted state we will set the image of large knob size then it is in an unselected state.

transportSlider.setThumbImage(UIImage(named: "Knob"), for: .normal)
transportSlider.setThumbImage(UIImage(named: "Knob-Tracking"), for: .highlighted)

This episode uses Xcode 10.2.1, Swift 5.0.