This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 10% discount for team members. Become a Subscriber

Instead of letting multiple view controllers manage the navigation bar's state individually, we pull this code out and unify the logic in one place.

0:06 Today we're going to work on our tvOS app, which we intend to release very early instead of spending a lot of time polishing it.

Toggling the Navigation Bar

0:31 Our current version has a bug that we need to fix before refactoring the code. When we select an episode from the episode list and open it, we see a double title; the navigation bar shows the title of the view controller, and this blocks a title label that we added to the view. We should hide the navigation bar in the episode view controller. Or it's the other way around actually: we only want to see the navigation bar when we're in the episode list.

1:06 Let's take a look at the EpisodeDetailViewController. We can hide the navigation bar by overriding viewWillAppear:

final public class EpisodeDetailViewController: UIViewController {
    // ...
    public override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(true, animated: animated)
    }
}

2:01 That fixes part of the problem, but when we go back to the episode list, the navigation bar stays hidden. We need to also modify the EpisodeListViewController to unhide the navigation bar, so we copy the code and replace true with false:

final public class EpisodesListViewController: UIViewController {
    // ...
    public override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(false, animated: animated)
    }
}

Improving the Architecture

2:45 That single piece of code, which toggles the navigation bar, is the only reason we had to write a specific view controller for the episode list. The rest of the functionality is covered by a generic table view controller from previous episodes, which is wrapped inside this custom view controller.

3:19 Other solutions we could've considered include adding a property to the generic table view controller that manages the navigation bar, somehow using a callback, or subclassing TableViewController.

3:35 But we have another problem with all these approaches: because we have to hide and show the navigation bar in different places, the code gets smeared out over different classes. This makes using one of these view controllers in a different place rather fragile and more difficult. It'd be better if we have one place — ideally outside the view controller — where we can manage the visibility of the navigation bar. From this central place, we can decide to only show the navigation bar in the root view controller of the hierarchy.

4:13 The way we architected this app is very similar to what we did in an early episode called Connecting View Controllers. Instead of using storyboards and segues, we have a class, App, that controls the flow of view controllers. The individual view controllers should have no knowledge about where they are in the application — for example, whether they're in a navigation controller or not. We violated this rule by making our view controllers deal with the navigation bar. In our current architecture, the App class should be the place from which to control the navigation bar visibility.

5:17 Let's fix that! We'll use a delegate that gets notified when we navigate between view controllers, and it'll control the navigation bar visibility. We make this delegate conform to UINavigationControllerDelegate, and we implement the protocol method navigationController(willShow:) to perform a specific task. Now the navigation bar is only visible when the root view controller is about to be shown:

final class EpisodesNavigationDelegate: NSObject, UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        let isRootVC = navigationController.viewControllers.first == viewController
        navigationController.setNavigationBarHidden(!isRootVC, animated: animated)
    }
}

7:41 Inside App, we instantiate the delegate and keep a strong reference to it. Then, where App creates the navigation controller, we can assign the delegate:

public final class App {
    // ...
    private let episodeNavigationDelegate = EpisodeNavigationDelegate()
    // ...
    private func videosTab() -> UIViewController {
        let navVC = screens.videoTab()
        navVC.delegate = episodeNavigationDelegate
        // ...
    }
}

8:49 This delegate replaces the two pieces of code with viewWillAppear from earlier, so they can be removed from EpisodesListViewController and EpisodeDetailViewController. This makes these view controllers less smart about where they are used inside our architecture, and that's a good thing.

Generics and Reusability

9:36 Let's move on to the next problem. We're using some specific code to constrain the table view to the readable content guide instead of allowing it to stretch out over the entire width of the screen. Again, we did this by wrapping the table view controller and replicating all of its API in the wrapper view controller. The only important part of the wrapper is the line where we constrain the view's edges. We should do better than this. Instead of creating an extra class, we can use the generic table view controller, along with a generic helper class that constrains the view to the readable content guide.

12:09 We rename the EpisodesListViewController to a more generic ReadableContentViewController that's initialized with a view controller. It adds this view controller to its children and copies the child's title. Then we remove all the table view code:

public final class ReadableContentViewController: UIViewController {
    private let child: UIViewController

    public init(_ child: UIViewController) {
        self.child = child
        super.init(nibName: nil, bundle: nil)
        self.title = child.title
    }

    // ...

    public override func viewDidLoad() {
        super.viewDidLoad()
        addChildViewController(child)
        view.addSubview(child.view)
        child.view.translatesAutoresizingMaskIntoConstraints = false
        child.view.constrainEdges(to: view.readableContentGuide)
        child.didMove(toParentViewController: self)
    }
}

13:41 Now we have to use this generic class instead of the specific EpisodesListViewController. Where our Screens class creates the episodes list table view controller, we wrap a generic table view controller in a ReadableContentViewController:

public final class Screens {
    // ...
    public func allEpisodes(/*...*/) -> UIViewController {
        let season = TableViewController(/*...*/)
        // ...
        return ReadableContentViewController(season)
    }
    // ...
}

14:44 To handle the child view controller's title more correctly, we should observe it; that way, we can update the parent's title whenever the child's title changes. But our current method works for now.

Future Improvements

15:00 We're making nice improvements toward a more flexible architecture. We removed all the boilerplate code by using our generic view controllers again and by abstracting away all the specifics.

15:58 A next step could be to improve EpisodeNavigationDelegate. We could continue the pattern and make this delegate more generic too, perhaps by passing in a closure to replace the single line that actually controls the navigation bar. All we'd have left in the generic delegate class is boilerplate code, which is reusable for creating other types of navigation controllers.

16:40 So far, we're already much happier with our code.