Episode #408

Player Bar Part 2

Series: Making a Podcast App From Scratch

15 minutes
Published on September 5, 2019

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

We take our player bar and install it into a custom tab bar. To do this we have to create a custom tab bar controller and tab bar subclass and mix it with just a little bit of questionable UIKit hackery to get it to layout how we want. We'll talk about the tradeoffs for different approaches as well as see some useful debugging tips when a button isn't responding to taps.

Creating a Custom Tab Bar

We will start by creating a class for our custom tab bar along with its controller. We will name it "ExtendedTabBarController" and "ExtendedTabBar" and hook them together in the storyboard. When the user taps on the present player button from our player bar, we will present the player from the ExtendedTabBarController. To do this, we will set the presentationRootController on the player bar to self when the tab bar controller is initialized.

import UIKit

class ExtendedTabBarController : UITabBarController {
    override func awakeFromNib() {
        super.awakeFromNib()

        let player = PlayerViewController.shared
        player.presentationRootController = self

        let playerBar = player.playerBar
        // install this in the tab bar
        (tabBar as? ExtendedTabBar)?.playerBar = playerBar
    }
}

Next, we will set the player bar instance from the controller and install it in the view hierarchy of the tab bar. In the event we ever wanted to call this setter twice, we will use willSet to remove any previous view before installing the new one.

class ExtendedTabBar : UITabBar {
    var playerBar: PlayerBar? {
        willSet {
            if newValue != playerBar {
                playerBar?.removeFromSuperview()
            }
        }
        didSet {
            if let playerBar = playerBar {
                install(playerBar: playerBar)
            }
        }
    }
}

In the tab bar controller, we will have an optional typecast for our extended tab bar.

// install this in the tab bar

(tabBar as? ExtendedTabBar)?.playerBar = playerBar

Installation of the Tab Bar

Finally, we will install the tab bar in our ExtendedTabBar. To avoid the conflicting constraints based on the current size of the storyboard we will set translatesAutoresizingMaskIntoConstraints to false. We will now set the constraints for playerBar. We will set out tab bar high so it accommodates the player bar.

var playerBarHeight: CGFloat = 0

private func install(playerBar: PlayerBar) {
    playerBarHeight = playerBar.frame.height
    addSubview(playerBar)
    playerBar.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        playerBar.heightAnchor.constraint(equalToConstant: playerBarHeight),
        playerBar.leadingAnchor.constraint(equalTo: leadingAnchor),
        playerBar.trailingAnchor.constraint(equalTo: trailingAnchor),
        playerBar.topAnchor.constraint(equalTo: topAnchor)
        ])
}

To determine the size to be rendered by layout before we start playing the player, we will override the sizeThatFits in our tab bar.

var playerBarVisible: Bool {
    return playerBar?.isHidden == false
}

override func sizeThatFits(_ size: CGSize) -> CGSize {
    let originalSize = super.sizeThatFits(size)

    guard playerBarVisible else { return originalSize }
    var modifiedSize = originalSize
    modifiedSize.height += playerBarHeight

    return modifiedSize
}

We now have to reposition the tab bar items back to where they would be in a normal height tab bar.

override func layoutSubviews() {
    super.layoutSubviews()
    guard playerBarVisible else { return }

    for subview in subviews {
        if let control = subview as? UIControl {
            control.frame.origin.y += playerBarHeight
            control.frame.size.height -= playerBarHeight
        }
    }
}

Debugging Tips for Nonresponding Buttons

To diagnose when buttons aren't tappable, we can check a few things:

1) We will check the User Interaction Enable for these buttons using View Debugger.
2) We will also check for the button is enabled and has target and action at run time.
3) Check if the button container is in view hierarchy to receive touch events. In this case, we will set the height of the button container equally to Stack View using a storyboard.

This episode uses Xcode 10.2.1, Swift 5.0.