Skip to content

Commit

Permalink
[AppBar] Inject AppBars after view controllers have been added to the…
Browse files Browse the repository at this point in the history
… navigation controller.

Before this change, it was never possible to interact with the navigationController property in a pushed view controller's viewDidLoad property when using MDCAppBarNavigationController. This behavior is surprising for folks using UINavigationController, which does allow you to access navigationController from within viewDidLoad.

This expectation is an anti-pattern (e.g. viewDidLoad is never guaranteed to be invoked after a view controller has been pushed onto a navigationController stack), but the goal of MDCAppBarNavigationController is to provide a drop-in replacement for UINavigationController so our intent is to align behaviors, even if they're not necessarily for the right reasons.

All that being said, the change introduced in this CL simply swaps the ordering of pushing the view controller and injecting the AppBar, such that the injection now happens after the super implementation of pushViewController has been invoked. This allows the viewDidLoad logic to be invoked after navigationController has been initialized on the view controller.

The reason we were doing it the other way before was so that the AppBar could intercept the relevant status bar appearance events. We work around that requirement in this change by invoking setNeedsStatusBarAppearanceUpdate and setNeedsUpdateOfHomeIndicatorAutoHidden immediately after injecting the AppBar.

PiperOrigin-RevId: 313410464
  • Loading branch information
Jeff Verkoeyen authored and material-automation committed May 27, 2020
1 parent 245b90c commit b78737b
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import MaterialComponents.MaterialAppBar
import MaterialComponents.MaterialAppBar_Theming
import MaterialComponents.MaterialContainerScheme

// This example demonstrates that it is possible to hide a view controller's
// navigationController.navigationBar during viewDidLoad when presented within an
// MDCAppBarNavigationController.
class AppBarNavigationControllerWithAppBarInitiallyHiddenExample:
UIViewController,
MDCAppBarNavigationControllerDelegate {
Expand Down Expand Up @@ -153,7 +156,6 @@ extension AppBarNavigationControllerWithAppBarInitiallyHiddenExample {
extension AppBarNavigationControllerWithAppBarInitiallyHiddenExample {

@objc func testPresented() {
// TODO(b/152510959): The AppBar is expected to be hidden, but it is not.
presentModal(animated: false)
}
}
37 changes: 27 additions & 10 deletions components/AppBar/src/MDCAppBarNavigationController.m
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ @interface MDCAppBarNavigationControllerInfo : NSObject
// This is intentionally a private protocol conformance in order to avoid public reliance on our
// conformance to this protocol.
@interface MDCAppBarNavigationController () <UIGestureRecognizerDelegate>
// Whether injected app bars should be hidden.
@property(nonatomic, assign) BOOL appBarHidden;
@end

@implementation MDCAppBarNavigationControllerInfo
Expand Down Expand Up @@ -77,6 +79,8 @@ - (void)MDCAppBarNavigationController_commonInit {
// We always want the UIKit navigation bar to be hidden; to do so we must invoke the super
// implementation.
[super setNavigationBarHidden:YES animated:NO];

_appBarHidden = NO;
}

- (void)viewDidLoad {
Expand All @@ -99,23 +103,27 @@ - (UIViewController *)childViewControllerForStatusBarStyle {

// Inject an App Bar, if necessary, when a view controller is pushed.
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
// We call this before invoking super because super immediately queries the pushed view controller
// for things like status bar style, which we want to have rerouted to our flexible header view
// controller.
[super pushViewController:viewController animated:animated];

[self injectAppBarIntoViewController:viewController];

[super pushViewController:viewController animated:animated];
[self setNeedsStatusBarAppearanceUpdate];
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
}

- (void)setViewControllers:(NSArray<UIViewController *> *)viewControllers animated:(BOOL)animated {
[super setViewControllers:viewControllers animated:animated];

for (UIViewController *viewController in viewControllers) {
// We call this before invoking super because super immediately queries the pushed view
// controller for things like status bar style, which we want to have rerouted to our flexible
// header view controller.
[self injectAppBarIntoViewController:viewController];
}

[super setViewControllers:viewControllers animated:animated];
[self setNeedsStatusBarAppearanceUpdate];
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
}

- (void)setNavigationBarHidden:(BOOL)navigationBarHidden {
Expand Down Expand Up @@ -166,6 +174,8 @@ - (void)appbar_setNavigationBarHidden:(BOOL)navigationBarHidden animated:(BOOL)a
- (void)appbar_setNavigationBarHidden:(BOOL)navigationBarHidden
animated:(BOOL)animated
forViewController:(UIViewController *)viewController {
self.appBarHidden = navigationBarHidden;

MDCAppBarViewController *appBarViewController =
[self appBarViewControllerForViewController:viewController];
if (!appBarViewController) {
Expand Down Expand Up @@ -249,8 +259,6 @@ - (void)injectAppBarIntoViewController:(UIViewController *)viewController {
// flexible header. In those cases the client is expected to create their own App Bar.
appBar.appBarViewController.headerView.observesTrackingScrollViewScrollEvents = YES;

appBar.appBarViewController.headerView.trackingScrollView = trackingScrollView;

appBar.appBarViewController.traitCollectionDidChangeBlock =
self.traitCollectionDidChangeBlockForAppBarController;

Expand All @@ -269,8 +277,17 @@ - (void)injectAppBarIntoViewController:(UIViewController *)viewController {
asChildOfViewController:viewController];
}

// Allow the delegates to configure the app bar's behavior before we inject the tracking scroll
// view in case features like observesTrackingScrollViewScrollEvents are disabled.
appBar.appBarViewController.headerView.trackingScrollView = trackingScrollView;

[viewController addChildViewController:appBar.appBarViewController];
[appBar addSubviewsToParent];

// Propagate the navigation bar visibility to the new app bar.
if (self.shouldSetNavigationBarHiddenHideAppBar && self.appBarHidden) {
[self appbar_setNavigationBarHidden:self.appBarHidden animated:NO];
}
}

- (BOOL)viewControllerHasFlexibleHeader:(UIViewController *)viewController {
Expand Down

0 comments on commit b78737b

Please sign in to comment.