00: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
00: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.
01: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)
}
}
02: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
02: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.
03: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
.
03: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.
04: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.
05: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)
}
}
07: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
}
}
08: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
09: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.