Material Design

App bars: top

Open bugs badge

The Material Design top app bar displays information and actions relating to the current view.

An animation showing a top app bar appearing and disappearing.

Design & API documentation

Table of contents


Overview

App bar is composed of the following components:

It is essentially a FlexibleHeader with a HeaderStackView and NavigationBar added as subviews.

MDCAppBarViewController is the primary API for the component. All integration strategies will make use of it in some manner. Unlike UIKit, which shares a single UINavigationBar instance across many view controllers in a stack, app bar relies on each view controller creating and managing its own MDCAppBarViewController instance.

Installation

Installation with CocoaPods

Add the following to your Podfile:

pod 'MaterialComponents/AppBar'

Then, run the following command:

pod install

Importing

To import the component:

Swift

import MaterialComponents.MaterialAppBar

Objective-C

#import "MaterialAppBar.h"

Usage

Typical use: View controller containment, as a navigation controller

The easiest integration path for using the app bar is through the MDCAppBarNavigationController. This API is a subclass of UINavigationController that automatically adds an MDCAppBarViewController instance to each view controller that is pushed onto it, unless an app bar or flexible header already exists.

When using the MDCAppBarNavigationController you will, at a minimum, need to configure the added app bar’s background color using the delegate.

Example

Swift

let navigationController = MDCAppBarNavigationController()
navigationController.pushViewController(<#T##viewController: UIViewController##UIViewController#>, animated: <#T##Bool#>)

// MARK: MDCAppBarNavigationControllerDelegate

func appBarNavigationController(_ navigationController: MDCAppBarNavigationController,
                                willAdd appBarViewController: MDCAppBarViewController,
                                asChildOf viewController: UIViewController) {
  appBarViewController.headerView.backgroundColor = <#(UIColor)#>
}

Objective-C

MDCAppBarNavigationController *navigationController =
    [[MDCAppBarNavigationController alloc] init];
[navigationController pushViewController:<#(nonnull UIViewController *)#> animated:<#(BOOL)#>];

#pragma mark - MDCAppBarNavigationControllerDelegate

- (void)appBarNavigationController:(MDCAppBarNavigationController *)navigationController
       willAddAppBarViewController:(MDCAppBarViewController *)appBarViewController
           asChildOfViewController:(UIViewController *)viewController {
  appBarViewController.headerView.backgroundColor = <#(nonnull UIColor *)#>;
}

Typical use: View controller containment, as a child

When an MDCAppBarViewController instance is added as a child to another view controller. In this case, the parent view controller is often the object that creates and manages the MDCAppBarViewController instance. This allows the parent view controller to configure the app bar directly.

You’ll typically push the parent onto a navigation controller, in which case you will also hide the navigation controller’s navigation bar using UINavigationController’s -setNavigationBarHidden:animated:.

Example

Swift

let appBarViewController = MDCAppBarViewController()

override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
  super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

  self.addChildViewController(appBarViewController)
}

override func viewDidLoad() {
  super.viewDidLoad()

  view.addSubview(appBarViewController.view)
  appBarViewController.didMove(toParentViewController: self)
}

Objective-C

@interface MyViewController ()
@property(nonatomic, strong, nonnull) MDCAppBarViewController *appBarViewController;
@end

@implementation MyViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  if (self) {
    _appBarViewController = [[MDCAppBarViewController alloc] init];

    [self addChildViewController:_appBarViewController];
  }
  return self;
}

- (void)viewDidLoad {
  [super viewDidLoad];

  [self.view addSubview:self.appBarViewController.view];
  [self.appBarViewController didMoveToParentViewController:self];
}

@end

Typical use: View controller containment, as a container

There are cases where adding an MDCAppBarViewController as a child is not possible, most notably:

  • UIPageViewController’s view is a horizontally-paging scroll view, meaning there is no fixed view to which an app bar could be added.
  • Any other view controller that animates its content horizontally without providing a fixed, non-horizontally-moving parent view.

In such cases, using MDCAppBarContainerViewController is preferred. MDCAppBarContainerViewController is a simple container view controller that places a content view controller as a sibling to an MDCAppBarViewController.

Note: the trade off to using this API is that it will affect your view controller hierarchy. If the view controller makes any assumptions about its parent view controller or its navigationController properties then these assumptions may break once the view controller is wrapped.

You’ll typically push the container view controller onto a navigation controller, in which case you will also hide the navigation controller’s navigation bar using UINavigationController’s -setNavigationBarHidden:animated:.

Example

Swift

let container = MDCAppBarContainerViewController(contentViewController: <#T##UIViewController#>)

Objective-C

MDCAppBarContainerViewController *container =
    [[MDCAppBarContainerViewController alloc] initWithContentViewController:<#(nonnull UIViewController *)#>];

Typical use: Tracking a scroll view

The flexible header can be provided with tracking scroll view. This allows the flexible header to expand, collapse, and shift off-screen in reaction to the tracking scroll view’s delegate events.

Important: When using a tracking scroll view you must forward the relevant UIScrollViewDelegate events to the flexible header.

Follow these steps to hook up a tracking scroll view:

Step 1: Set the tracking scroll view.

In your viewDidLoad, set the trackingScrollView property on the header view:

Swift

headerViewController.headerView.trackingScrollView = scrollView

Objective-C

self.headerViewController.headerView.trackingScrollView = scrollView;

scrollView might be a table view, collection view, or a plain UIScrollView.

Step 2: Forward UIScrollViewDelegate events to the Header View.

There are two ways to forward scroll events.

Option 1: if your controller does not need to respond to UIScrollViewDelegate events and you’re using either a plain UIScrollView or a UITableView you can set your MDCFlexibleHeaderViewController instance as the scroll view’s delegate.

Swift

scrollView.delegate = headerViewController

Objective-C

scrollView.delegate = self.headerViewController;

Option 2: implement the required UIScrollViewDelegate methods and forward them to the MDCFlexibleHeaderView instance. This is the most flexible approach and will work with any UIScrollView subclass.

Swift

// MARK: UIScrollViewDelegate

override func scrollViewDidScroll(scrollView: UIScrollView) {
  if scrollView == headerViewController.headerView.trackingScrollView {
    headerViewController.headerView.trackingScrollViewDidScroll()
  }
}

override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
  if scrollView == headerViewController.headerView.trackingScrollView {
    headerViewController.headerView.trackingScrollViewDidEndDecelerating()
  }
}

override func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  let headerView = headerViewController.headerView
  if scrollView == headerView.trackingScrollView {
    headerView.trackingScrollViewDidEndDraggingWillDecelerate(decelerate)
  }
}

override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
  let headerView = headerViewController.headerView
  if scrollView == headerView.trackingScrollView {
    headerView.trackingScrollViewWillEndDraggingWithVelocity(velocity, targetContentOffset: targetContentOffset)
  }
}

Objective-C

#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  if (scrollView == self.headerViewController.headerView.trackingScrollView) {
    [self.headerViewController.headerView trackingScrollViewDidScroll];
  }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
  if (scrollView == self.headerViewController.headerView.trackingScrollView) {
    [self.headerViewController.headerView trackingScrollViewDidEndDecelerating];
  }
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
  if (scrollView == self.headerViewController.headerView.trackingScrollView) {
    [self.headerViewController.headerView trackingScrollViewDidEndDraggingWillDecelerate:decelerate];
  }
}

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
                     withVelocity:(CGPoint)velocity
              targetContentOffset:(inout CGPoint *)targetContentOffset {
  if (scrollView == self.headerViewController.headerView.trackingScrollView) {
    [self.headerViewController.headerView trackingScrollViewWillEndDraggingWithVelocity:velocity
                                                                    targetContentOffset:targetContentOffset];
  }
}

Enabling observation of the tracking scroll view

If you do not require the flexible header’s shift behavior, then you can avoid having to manually forward UIScrollViewDelegate events to the flexible header by enabling observesTrackingScrollViewScrollEvents on the flexible header view. Observing the tracking scroll view allows the flexible header to over-extend, if enabled, and allows the header’s shadow to show and hide itself as the content is scrolled.

Note: if you support pre-iOS 11 then you will also need to explicitly clear your tracking scroll view in your deinit/dealloc method.

Swift

flexibleHeaderViewController.headerView.observesTrackingScrollViewScrollEvents = true

deinit {
  // Required for pre-iOS 11 devices because we've enabled observesTrackingScrollViewScrollEvents.
  appBarViewController.headerView.trackingScrollView = nil
}

Objective-C

flexibleHeaderViewController.headerView.observesTrackingScrollViewScrollEvents = YES;

- (void)dealloc {
  // Required for pre-iOS 11 devices because we've enabled observesTrackingScrollViewScrollEvents.
  self.appBarViewController.headerView.trackingScrollView = nil;
}

Note: if observesTrackingScrollViewScrollEvents is enabled then you can neither enable shift behavior nor manually forward scroll view delegate events to the flexible header.

UINavigationItem support

The App Bar begins mirroring the state of your view controller’s navigationItem in the provided navigationBar once you call addSubviewsToParent.

Learn more by reading the Navigation Bar section on Observing UINavigationItem instances. Notably: read the section on “Exceptions” to understand which UINavigationItem are not supported.

Interactive background views

Scenario: you’ve added a background image to your App Bar and you’d now like to be able to tap the background image.

This is not trivial to do with the App Bar APIs due to considerations being discussed in Issue #184.

The heart of the limitation is that we’re using a view (headerStackView) to lay out the Navigation Bar. If you add a background view behind the headerStackView instance then headerStackView will end up eating all of your touch events.

Until Issue #184 is resolved, our recommendation for building interactive background views is the following:

  1. Do not use the App Bar component.
  2. Create your own Flexible Header. Learn more by reading the Flexible Header Usage docs.
  3. Add your views to this flexible header instance.
  4. Create a Navigation Bar if you need one. Treat it like any other custom view.

Adjusting the top layout guide of a view controller

If your content view controller depends on the top layout guide being adjusted — e.g. if the content does not have a tracking scroll view and therefore relies on the top layout guide to perform layout calculations — then you should consider setting topLayoutGuideViewController to the content view controller.

Setting this property does two things:

  1. Adjusts the view controller’s topLayoutGuide property to take the flexible header into account (most useful pre-iOS 11).
  2. On iOS 11 and up — if there is no tracking scroll view — also adjusts the additionalSafeAreaInsets property to take the flexible header into account.

Note: topLayoutGuideAdjustmentEnabled is automatically enabled if this property is set.

Swift

flexibleHeaderViewController.topLayoutGuideViewController = contentViewController

Objective-C

flexibleHeaderViewController.topLayoutGuideViewController = contentViewController;

Behavioral flags

A behavioral flag is a temporary API that is introduced to allow client teams to migrate from an old behavior to a new one in a graceful fashion. Behavioral flags all go through the following life cycle:

  1. The flag is introduced. The default is chosen such that clients must opt in to the new behavior.
  2. After some time, the default changes to the new behavior and the flag is marked as deprecated.
  3. After some time, the flag is removed.

The app bar component and its dependencies include a variety of flags that affect the behavior of the MDCAppBarViewController. Many of these flags represent feature flags that we are using to allow client teams to migrate from an old behavior to a new, usually less-buggy one.

You are encouraged to set all of the behavioral flags immediately after creating an instance of the app bar.

The minimal set of recommended flag values are:

Swift

// Enables support for iPad popovers and extensions.
// Automatically enables topLayoutGuideAdjustmentEnabled as well, but does not set a
// topLayoutGuideViewController.
appBarViewController.inferTopSafeAreaInsetFromViewController = true

// Enables support for iPhone X safe area insets.
appBarViewController.headerView.minMaxHeightIncludesSafeArea = false

Objective-C

// Enables support for iPad popovers and extensions.
// Automatically enables topLayoutGuideAdjustmentEnabled as well, but does not set a
// topLayoutGuideViewController.
appBarViewController.inferTopSafeAreaInsetFromViewController = YES;

// Enables support for iPhone X safe area insets.
appBarViewController.headerView.minMaxHeightIncludesSafeArea = NO;

Removing safe area insets from the min/max heights

The minimum and maximum height values of the flexible header view assume by default that the values include the top safe area insets value. This assumption no longer holds true on devices with a physical safe area inset and it never held true when flexible headers were shown in non full screen settings (such as popovers on iPad).

This behavioral flag is enabled by default, but will eventually be disabled by default and the flag will eventually be removed.

Swift

flexibleHeaderViewController.headerView.minMaxHeightIncludesSafeArea = false

Objective-C

flexibleHeaderViewController.headerView.minMaxHeightIncludesSafeArea = NO;

Enabling top layout guide adjustment

The topLayoutGuideAdjustmentEnabled behavior flag affects topLayoutGuideViewController. Setting topLayoutGuideAdjustmentEnabled to YES enables the new behavior.

topLayoutGuideAdjustmentEnabled is disabled by default, but will eventually be enabled by default and the flag will eventually be removed.

Swift

flexibleHeaderViewController.topLayoutGuideAdjustmentEnabled = true

Objective-C

flexibleHeaderViewController.topLayoutGuideAdjustmentEnabled = YES;

Enabling inferred top safe area insets

Prior to this behavioral flag, the flexible header always assumed that it was presented in a full-screen capacity, meaning it would be placed directly behind the status bar or device bezel (such as the iPhone X’s notch). This assumption does not support extensions and iPad popovers.

Enabling the inferTopSafeAreaInsetFromViewController flag tells the flexible header to use its view controller ancestry to extract a safe area inset from its context, instead of relying on assumptions about placement of the header.

This behavioral flag is disabled by default, but will eventually be enabled by default and the flag will eventually be removed.

Swift

flexibleHeaderViewController.inferTopSafeAreaInsetFromViewController = true

Objective-C

flexibleHeaderViewController.inferTopSafeAreaInsetFromViewController = YES;

Note: if this flag is enabled and you’ve also provided a topLayoutGuideViewController, take care that the topLayoutGuideViewController is not a direct ancestor of the flexible header or your app will enter an infinite loop. As a general rule, your topLayoutGuideViewController should be a sibling to the flexible header.

See the FlexibleHeader documentation for additional usage guides.

Extensions

Color Theming

You can theme an app bar with your app’s color scheme using the ColorThemer extension.

You must first add the Color Themer extension to your project:

pod 'MaterialComponents/AppBar+ColorThemer'

Swift

// Step 1: Import the ColorThemer extension
import MaterialComponents.MaterialAppBar_ColorThemer

// Step 2: Create or get a color scheme
let colorScheme = MDCSemanticColorScheme()

// Step 3: Apply the color scheme to your component
MDCAppBarColorThemer.applySemanticColorScheme(colorScheme, to: component)

Objective-C

// Step 1: Import the ColorThemer extension
#import "MaterialAppBar+ColorThemer.h"

// Step 2: Create or get a color scheme
id<MDCColorScheming> colorScheme = [[MDCSemanticColorScheme alloc] init];

// Step 3: Apply the color scheme to your component
[MDCAppBarColorThemer applySemanticColorScheme:colorScheme
     toAppBar:component];

Typography Theming

You can theme an app bar with your app’s typography scheme using the TypographyThemer extension.

You must first add the Typography Themer extension to your project:

pod 'MaterialComponents/AppBar+TypographyThemer'

Swift

// Step 1: Import the TypographyThemer extension
import MaterialComponents.MaterialAppBar_TypographyThemer

// Step 2: Create or get a typography scheme
let typographyScheme = MDCTypographyScheme()

// Step 3: Apply the typography scheme to your component
MDCAppBarTypographyThemer.applyTypographyScheme(typographyScheme, to: component)

Objective-C

// Step 1: Import the TypographyThemer extension
#import "MaterialAppBar+TypographyThemer.h"

// Step 2: Create or get a typography scheme
id<MDCTypographyScheming> typographyScheme = [[MDCTypographyScheme alloc] init];

// Step 3: Apply the typography scheme to your component
[MDCAppBarTypographyThemer applyTypographyScheme:colorScheme
     toAppBar:component];

Accessibility

MDCAppBar Accessibility

Because the App Bar mirrors the state of your view controller’s navigationItem, making an App Bar accessible often does not require any extra work.

See the following examples:

Objective-C
self.navigationItem.rightBarButtonItem =
   [[UIBarButtonItem alloc] initWithTitle:@"Right"
                                    style:UIBarButtonItemStyleDone
                                   target:nil
                                   action:nil];

NSLog(@"accessibilityLabel: %@",self.navigationItem.rightBarButtonItem.accessibilityLabel);
// Prints out "accessibilityLabel: Right"
Swift
self.navigationItem.rightBarButtonItem =
    UIBarButtonItem(title: "Right", style: .done, target: nil, action: nil)

print("accessibilityLabel: \(self.navigationItem.rightBarButtonItem.accessibilityLabel)")
// Prints out "accessibilityLabel: Right"

Migration guides

Migration guide: MDCAppBar to MDCAppBarViewController

Deprecation schedule:

  • October 15, 2018: MDCAppBar and any references to it in MDC will deprecated.
  • November 15, 2018: MDCAppBar and any references to it in MDC will be deleted.

MDCAppBarViewController is a direct replacement for MDCAppBar. The migration essentially looks like so:

// Step 1
-  let appBar = MDCAppBar()
+  let appBarViewController = MDCAppBarViewController()

// Step 2
-  self.addChildViewController(appBar.headerViewController)
+  self.addChildViewController(appBarViewController)

// Step 3
-  appBar.addSubviewsToParent()
+  view.addSubview(appBarViewController.view)
+  appBarViewController.didMove(toParentViewController: self)

MDCAppBarViewController is a subclass of MDCFlexibleHeaderViewController, meaning you configure an MDCAppBarViewController instance exactly the same way you’d configure an MDCFlexibleHeaderViewController instance.

MDCAppBar also already uses MDCAppBarViewController under the hood so you can directly replace any references of appBar.headerViewController with appBarViewController.

Swift find and replace recommendations

Find Replace
let appBar = MDCAppBar() let appBarViewController = MDCAppBarViewController()
self.addChildViewController(appBar.headerViewController) self.addChildViewController(appBarViewController)
appBar.addSubviewsToParent() view.addSubview(appBarViewController.view)
appBarViewController.didMove(toParentViewController: self)
self.appBar.headerViewController self.appBarViewController

Objective-C find and replace recommendations

Find Replace
MDCAppBar *appBar; MDCAppBarViewController *appBarViewController;
appBar = [[MDCAppBar alloc] init] appBarViewController = [[MDCAppBarViewController alloc] init]
addChildViewController:appBar.headerViewController addChildViewController:appBarViewController
[self.appBar addSubviewsToParent]; [self.view addSubview:self.appBarViewController.view];
[self.appBarViewController didMoveToParentViewController:self];

Example migrations