Flexible header

Open bugs badge

A flexible header is a container view whose height and vertical offset react to UIScrollViewDelegate events.

An animation showing a flexible header appearing and disappearing.

Design & API documentation

Table of contents


Overview

A flexible header is a simple container view designed to live at the top of a view controller and react to scroll view events. Flexible headers are intended to be created and owned by each view controller that requires one. This is an intentional deviation from the one-UINavigationBar design of UINavigationController, and we discuss the merits and drawbacks of this approach below.

The heart of flexible header is MDCFlexibleHeaderView. MDCFlexibleHeaderView is a container view, meaning you are expected to register your own subviews to it. MDCFlexibleHeaderView simply manages its "frame", you are responsible for everything within the bounds.

MDCFlexibleHeaderViewController is the ideal way to create and manage the lifetime of a MDCFlexibleHeaderView instance. Adding this view controller as a child of your view controller ensures that the flexible header is able to react to device orientation and view appearance events. This document generally assumes that you are familiar with UIViewController containment.

Considerations

Requiring each view controller to own a flexible header instance has several technical advantages:

  • Transitions between two view controllers can include the header in their motion considerations.
  • Flexible header customizations are scoped to the owner view controller.

It also has some technical disadvantages:

  • There is a cost to registering and owning a flexible header instance when compared to UINavigationController and the free availability of UINavigationBar. Improvements to this are being discussed on issue #268.

Installation

Installation with CocoaPods

Add the following to your Podfile:

Then, run the following command:

Importing

To import the component:

Swift

Objective-C

Usage

Typical use: Add the flexible header to a view controller

Each view controller in your app that intends to manage its own flexible header will follow these instructions. You'll typically add the flexible header to the same view controllers that you'd push onto a UINavigationController, hiding the UINavigationController's navigationBar accordingly.

The result of following these steps will be that:

  1. a flexible header is registered as a child view controller of your view controller, and that
  2. you have access to a MDCFlexibleHeaderView instance via the headerView property on your MDCFlexibleHeaderViewController instance.

Step 1: Create an instance of MDCFlexibleHeaderViewController.

MDCFlexibleHeaderViewController is a UIViewController that manages the relationship of your view controller to a MDCFlexibleHeaderView instance.

Swift

Objective-C

Step 2: Add the MDCFlexibleHeaderViewController's view to your view controller's view.

Ideally you will do this after all views have been added to your controller's view in order to ensure that the flexible header is in front of all other views.

Swift

Objective-C

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

Objective-C

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

iOS 13 Collection Considerations

iOS 13 changed the behavior of the contentInset of a collection view by triggering a layout. This may affect your app if you have not yet registered cells for reuse yet. Our recomendation is to use view controller composition by making your collection view controller a child view controller. If this is not possible then ensure the correct order of operations by registering cell reuse identifiers before setting the Flexible Header's trackingScrollView.

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

Objective-C

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

Objective-C

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

Objective-C

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

Shifting a flexible header off-screen

A flexible header that tracks a scroll view will expand and contract its height in reaction to scroll view events. A flexible header can also shift off-screen in reaction to scroll view events by changing the flexible header's behavior.

Swift

Objective-C

Important: when a flexible header shifts off-screen it will not hide the content views. Your content views are responsible for hiding themselves in reaction to the flexible header shifting off-screen. Read the section on Reacting to frame changes for more information.

It is also possible to hide the status bar when shifting the flexible header off-screen. Enable this behavior by setting the enabledWithStatusBar behavior and implementing childViewControllerForStatusBarHidden on the parent view controller.

Swift

Objective-C

If you would like to be able to show and hide your flexible header similar to how UINavigationBar allows the navigation bar to be shown and hidden, you can use the hideable shift behavior. This behavior will allow you to toggle visibility of the header using the shiftHeaderOffScreenAnimated: and shiftHeaderOnScreenAnimated: APIs only; the user will not be able to drag the header either on or off-screen.

Swift

Objective-C

Reacting to frame changes

In order to react to flexible header frame changes you can set yourself as the MDCFlexibleHeaderViewController instance's layoutDelegate.

Swift

Objective-C

Utilizing Top Layout Guide on Parent View Controller

When pairing MDCFlexibleHeaderViewController with a view controller, it may be desirable to use the paired view controller's topLayoutGuide to constrain additionals views. To constrain the topLayoutGuide to the bottom point of the MDCFlexibleHeaderViewController, call updateTopLayoutGuide on the flexible header view controller within the paired view controller's viewWillLayoutSubviews method.

Swift

Objective-C

Subclassing considerations

A subclass of your view controller may add additional views in their viewDidLoad, potentially resulting in the header being covered by the new views. It is the responsibility of the subclass to take the z-index into account:

Swift

Objective-C

Interacting with UINavigationController

Push a view controller with a flexible header onto UINavigationController and you may find that the existing UINavigationBar is undesired. The most obvious example occurs when your flexible header has its own navigation bar.

If this is the case then we recommend hiding the UINavigationController's navigationBar during UIViewController appearance events: viewWillAppear: or viewWillDisappear:. Changing the navigation bar's visibility during these events gives the highest likelihood of your navigation bar animating in/out in a reasonable manner.

Important: Hiding UINavigationController's navigationBar nullifies UINavigationController's swipe- to-go-back feature. To continue using this feature whilst hiding the navigationBar, read the section on Enabling Swipe to Go Back With Hidden NavigationBar.

Swift

Objective-C

Add the following to view controllers that don't have an app bar:

Swift

Objective-C

If all of your view controllers use the App Bar in a given UINavigationController then you can simply hide the navigationBar when you create the navigation controller. Don't forget to do this at app restoration time!

Swift

Objective-C

Enabling Swipe to Dismiss

When using MDCFlexibileHeaderController within a UINavigationController, setting the UINavigationController's navigationBarHidden property to YES results in the loss of the swipe-to-go-back feature associated with the controller.

To re-enable this feature whilst hiding the navigation controller's navigationBar we recommend setting a pointer to the current interactivePopGestureRecognizer's delegate in the viewWillAppear: method before setting the navigationBarHidden property to YES, setting the interactivePopGestureRecognizer's delegate to nil while the MDCFlexibileHeaderController's parent controller is actively on-screen in viewDidAppear:, then re-setting the interactivePopGestureRecognizer's delegate to the held pointer in the viewWillDisappear: method.

Swift

Objective-C

Status bar style

MDCHeaderViewController instances are able to recommend a status bar style by inspecting the background color of the MDCFlexibleHeaderView. If you'd like to use this logic to automatically update your status bar style, implement childViewControllerForStatusBarStyle in your app's view controller.

Swift

Objective-C

Background images

This example shows how to add a custom background image view to a flexible header.

You can create and add a UIImageView subview to the flexible header view's content view:

Swift

Objective-C

Notes:

  • Add the image view to the header view's contentView, not the header view itself.
  • Set the contentMode to "ScaleAspectFill" to ensure that the image always fills the available header space, even if the image is too small. This is usually preferred, but consider changing the contentMode if you want a different behavior.
  • Enable clipsToBounds in order to ensure that your image view does not bleed past the bounds of the header view. The header view's clipsToBounds is disabled by default.

Touch forwarding

The flexible header allows you to forward touch events to the tracking scroll view. This provides the illusion that the flexible header is part of the tracking scroll view.

Starting touch forwarding

To start touch forwarding you must call forwardTouchEventsForView: with each view:

Swift

Objective-C

Stopping touch forwarding

To stop touch forwarding you must call forwardTouchEventsForView: with each view:

Swift

Objective-C

Tracking a parent view

While we do not recommend it, there are situations in which the trackingScrollView will be the parent view of the flexible header's view. The most notable example is UITableViewController, whose viewis the UITableView instance, so there is no other view to register the tracking scroll view to.

As you might expect, this situation causes the flexible header to scroll off-screen with the scroll view regardless of the flexible header's scrolling behavior. To counter this, the flexible header sets its transform to an inversion of the current contentOffset. This gives the illusion of the flexible header staying fixed in place, even though the underlying scroll view is scrolling.

In these situations the flexible header also ensures that it is always the front-most view. This is to combat the UITableView displaying its divider lines in front of the flexible header.

WKWebView considerations

When a WKWebView with content that is smaller than the screen is set as a tracking scroll view for a flexible header, the WKWebView's scroll view may not correctly calculate its contentSize.height. This bug manifests as a small web page that is scrollable when it shouldn't be and can most easily be reproduced by loading a simple HTML string into a WKWebView with a single word in the body tag.

To fix this bug, at a minimum you must enable the new runtime behavior useAdditionalSafeAreaInsetsForWebKitScrollViews and set a topLayoutGuideViewController. Doing so will fix the bug on iOS 11 and up.

Swift

Objective-C

If you support any OS below iOS 11, you'll also need to adjust the frame of your WKWebView on devices running these older operating systems so that the web view is aligned to the top layout guide.

Swift

Objective-C

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 flexible header component includes a variety of flags that affect the behavior of the MDCFlexibleHeaderViewController. 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 flexible header.

The minimal set of recommended flag values are:

Swift

Objective-C

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

Objective-C

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

Objective-C

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

Objective-C

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.

Migration guides

Migration guide: minMaxHeightIncludesSafeArea

Deprecation schedule:

  • October 16, 2018: minMaxHeightIncludesSafeArea will be disabled by default.
  • October 23, 2018: minMaxHeightIncludesSafeArea will be marked deprecated.
  • November 23, 2018: minMaxHeightIncludesSafeArea will be deleted.

minMaxHeightIncludesSafeArea is a behavioral flag on MDCFlexibleHeaderView that must be disabled to ensure iPhone X compatibility.

When this property is enabled (the legacy behavior), the minimumHeight and maximumHeight values are expected to include the device's top safe area insets in their value. This means it is the responsibility of the client to update these height values with the values of the top safe area insets.

When you disable this property you are expected to set minimumHeight and maximumHeight to only the height of the content that would be displayed below the top safe area insets.

We intend to eventually disableminMaxHeightIncludesSafeArea by default and remove the property altogether. As such, you are encouraged to proactively disable this property now anywhere that you use a FlexibleHeader.

Example usage:

Swift

Objective-C