blob: fd8c81f6f34788cf4557cd2b22de1d8bd7a3e82e [file] [log] [blame] [edit]
// Copyright 2015-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "MDCFlexibleHeaderViewController.h"
#import "private/MDCFlexibleHeaderHairline.h"
#import "private/MDCFlexibleHeaderView+Private.h"
#import "MaterialAvailability.h"
#import "MDCFlexibleHeaderContainerViewController.h"
#import "MDCFlexibleHeaderSafeAreaDelegate.h"
#import "MDCFlexibleHeaderView+ShiftBehavior.h"
#import "MDCFlexibleHeaderView.h"
#import "MDCFlexibleHeaderViewDelegate.h"
#import "MDCFlexibleHeaderViewLayoutDelegate.h"
#import "MaterialFlexibleHeader+ShiftBehaviorEnabledWithStatusBar.h"
#import "MaterialApplication.h"
#import "MaterialUIMetrics.h"
#import <MDFTextAccessibility/MDFTextAccessibility.h>
@interface UIView ()
- (UIEdgeInsets)safeAreaInsets; // For pre-iOS 11 SDK targets.
@end
static inline UIStatusBarStyle StatusBarStyleOnBackgroundColor(UIColor *color) {
if (CGColorGetAlpha(color.CGColor) < 1) {
return UIStatusBarStyleDefault;
}
// We assume that the light iOS status text is white and not big enough to be considered "large"
// text according to the W3CAG 2.0 spec.
if ([MDFTextAccessibility textColor:[UIColor whiteColor]
passesOnBackgroundColor:color
options:MDFTextAccessibilityOptionsNone]) {
return UIStatusBarStyleLightContent;
}
#if MDC_AVAILABLE_SDK_IOS(13_0)
if (@available(iOS 13.0, *)) {
return UIStatusBarStyleDarkContent;
}
#endif // MDC_AVAILABLE_SDK_IOS(13_0)
return UIStatusBarStyleDefault;
}
// KVO contexts
static char *const kKVOContextMDCFlexibleHeaderViewController =
"kKVOContextMDCFlexibleHeaderViewController";
@interface MDCFlexibleHeaderViewController () <MDCFlexibleHeaderViewDelegate>
/*
The current height offset of the flexible header controller with the addition of the current status
bar state at any given time.
This property is used to determine the bottom point of the |flexibleHeaderView| within the window.
*/
@property(nonatomic) CGFloat flexibleHeaderViewControllerHeightOffset;
/*
This is the target top layout guide that we will modify such that it includes the flexible header's
height and any top safe area insets we were able to infer.
We hold a strong reference to it because on pre-iOS 11 devices UIKit will attempt to reset the
top layout guide. We observe (using KVO) any changes made to the constraint and reset the
constraint's length when a modification outside of our awareness occurs.
*/
@property(nonatomic, strong) NSLayoutConstraint *topLayoutGuideConstraint;
/*
For avoiding re-entrant recursion while modifying the top layout guide.
*/
@property(nonatomic) BOOL isUpdatingTopLayoutGuide;
/*
On pre-iOS 11 devices, we use this layout constraint to extract the top safe area inset from the
root ancestor view controller.
*/
@property(nonatomic, strong) NSLayoutConstraint *topSafeAreaConstraint;
/*
On iOS 11+ devices, we use this view to extract the top safe area inset from the root ancestor view
controller.
*/
@property(nonatomic, strong) UIView *topSafeAreaView;
/**
Supports the behavior of showing a narrow line at the bottom edge of the flexible header view.
*/
@property(nonatomic, strong) MDCFlexibleHeaderHairline *hairline;
@property(nonatomic, getter=isTopLayoutGuideAdjustmentEnabled) BOOL topLayoutGuideAdjustmentEnabled;
@property(nonatomic)
BOOL permitInferringTopSafeAreaFromTopLayoutGuideViewController NS_AVAILABLE_IOS(11.0);
@end
@implementation MDCFlexibleHeaderViewController
@synthesize preferredStatusBarStyle = _preferredStatusBarStyle;
- (void)dealloc {
// Clear KVO observers
self.topLayoutGuideConstraint = nil;
self.topSafeAreaConstraint = nil;
self.topSafeAreaView = nil;
}
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
[self commonMDCFlexibleHeaderViewControllerInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonMDCFlexibleHeaderViewControllerInit];
}
return self;
}
- (void)commonMDCFlexibleHeaderViewControllerInit {
_inferPreferredStatusBarStyle = YES;
MDCFlexibleHeaderView *headerView =
[[MDCFlexibleHeaderView alloc] initWithFrame:[UIScreen mainScreen].bounds];
headerView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
headerView.delegate = self;
_headerView = headerView;
self.hairline = [[MDCFlexibleHeaderHairline alloc] initWithContainerView:_headerView];
}
- (void)loadView {
self.view = self.headerView;
}
- (void)willMoveToParentViewController:(UIViewController *)parent {
[super willMoveToParentViewController:parent];
BOOL shouldDisableAutomaticInsetting = YES;
// Prior to iOS 11 there was no way to know whether UIKit had injected insets into our
// UIScrollView, so we disable automatic insetting on these devices. iOS 11 provides
// the adjustedContentInset API which allows us to respond to changes in the safe area
// insets, so on iOS 11 we actually expect iOS to manage the safe area insets.
if (@available(iOS 11.0, *)) {
shouldDisableAutomaticInsetting = NO;
}
if (shouldDisableAutomaticInsetting) {
parent.automaticallyAdjustsScrollViewInsets = NO;
}
}
- (void)didMoveToParentViewController:(UIViewController *)parent {
[super didMoveToParentViewController:parent];
#if TARGET_OS_UIKITFORMAC
// When running under UIKit for Mac, the window size may not match the value returned by
// `[UIScreen mainScreen].bounds` when `commonMDCFlexibleHeaderViewControllerInit` was
// called, so the width will be updated here to match the hosting view.
CGRect headerFrame = _headerView.frame;
headerFrame.size.width = CGRectGetWidth(_headerView.superview.bounds);
_headerView.frame = headerFrame;
#endif
// The header depends on the tracking scroll view to know how tall it should be.
// If there is no tracking scroll view then we have to poke the header into sizing itself.
if (!_headerView.trackingScrollView) {
[_headerView sizeToFit];
} else if (!_headerView.observesTrackingScrollViewScrollEvents) {
[_headerView trackingScrollViewDidScroll];
}
if (self.topLayoutGuideAdjustmentEnabled) {
[self updateTopLayoutGuide];
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.inferTopSafeAreaInsetFromViewController) {
// At this point we can be confident that our view controller ancestry is as complete as
// possible, so we can now infer our top safe area source view controller.
[self fhv_inferTopSafeAreaSourceViewController];
// Querying the top layout guide ensures that the flexible header receives layout event when
// the status bar visibility changes. This allows the flexible header to animate alongside any
// status bar visibility changes.
[self.parentViewController topLayoutGuide];
}
if (self.topLayoutGuideAdjustmentEnabled) {
[self updateTopLayoutGuide];
} else {
// Legacy behavior.
for (NSLayoutConstraint *constraint in self.parentViewController.view.constraints) {
// Because topLayoutGuide is a readonly property on a viewController we must manipulate
// the present one via the NSLayoutConstraint attached to it. Thus we keep reference to it.
if (constraint.firstItem == self.parentViewController.topLayoutGuide &&
constraint.secondItem == nil) {
self.topLayoutGuideConstraint = constraint;
}
}
// On moving to parentViewController, we calculate the height
self.flexibleHeaderViewControllerHeightOffset = [self headerViewControllerHeight];
}
#if DEBUG
NSAssert(![self.parentViewController.parentViewController
isKindOfClass:[MDCFlexibleHeaderContainerViewController class]],
@"An instance of %@ has been injected into a view controller (%@) that is already"
@" wrapped by an instance of %@ - this is not allowed and will cause double headers to"
@" appear. Choose to either wrap or inject your view controller (preferring injection"
@" where possible).",
NSStringFromClass([self class]), NSStringFromClass([self.parentViewController class]),
NSStringFromClass([MDCFlexibleHeaderContainerViewController class]));
#endif
}
- (UIStatusBarStyle)preferredStatusBarStyle {
if (self.inferPreferredStatusBarStyle) {
UIColor *backgroundColor =
[MDCFlexibleHeaderView appearance].backgroundColor ?: _headerView.backgroundColor;
return StatusBarStyleOnBackgroundColor(backgroundColor);
} else {
return _preferredStatusBarStyle;
}
}
- (void)setPreferredStatusBarStyle:(UIStatusBarStyle)preferredStatusBarStyle {
NSAssert(!self.inferPreferredStatusBarStyle,
@"You must disable inferPreferredStatusBarStyle prior to setting a status bar style.");
_preferredStatusBarStyle = preferredStatusBarStyle;
}
- (BOOL)prefersStatusBarHidden {
return _headerView.prefersStatusBarHidden;
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[_headerView viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (self.topLayoutGuideAdjustmentEnabled) {
[self updateTopLayoutGuide];
}
}
#pragma mark TraitCollection
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == _headerView.trackingScrollView) {
[_headerView trackingScrollViewDidScroll];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (scrollView == _headerView.trackingScrollView) {
[_headerView trackingScrollViewDidEndDecelerating];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (scrollView == _headerView.trackingScrollView) {
[_headerView trackingScrollViewDidEndDraggingWillDecelerate:decelerate];
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset {
if (scrollView == _headerView.trackingScrollView) {
[_headerView trackingScrollViewWillEndDraggingWithVelocity:velocity
targetContentOffset:targetContentOffset];
}
}
#pragma mark KVO
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == kKVOContextMDCFlexibleHeaderViewController) {
void (^mainThreadWork)(void) = ^{
if (object == self->_topLayoutGuideConstraint && self.topLayoutGuideAdjustmentEnabled) {
[self updateTopLayoutGuide];
}
if (self.inferTopSafeAreaInsetFromViewController &&
(object == self->_topSafeAreaConstraint || object == self->_topSafeAreaView)) {
[self->_headerView topSafeAreaInsetDidChange];
}
};
// Ensure that UIKit modifications occur on the main thread.
if ([NSThread isMainThread]) {
mainThreadWork();
} else {
[[NSOperationQueue mainQueue] addOperationWithBlock:mainThreadWork];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - Top layout guide support
/*
When the flexible header's height changes, we want to adjust the topLayoutGuide length of the
content view controller so that its content can adjust accordingly. This is the same behavior that
UIKit container view controllers provide.
Unfortunately, topLayoutGuide is a read-only property on UIViewController with no way to
override it, and no public setter for the length.
The only known way to modify this property programmatically is to access the view controller's
view constraints and extract the first constraint that contains the top layout guide (and only
the top layout guide). Modifying the "constant" property of this constraint has the
undocumented side effect of also updating the topLayoutGuide's length.
This approach is discussed here:
https://stackoverflow.com/questions/19588171/how-to-set-toplayoutguide-position-for-child-view-controller
Also see this open radar feature request for a mutable top layout guide:
http://www.openradar.me/19984939
*/
- (void)fhv_extractTopLayoutGuideConstraint {
UIViewController *topLayoutGuideViewController =
[self fhv_topLayoutGuideViewControllerWithFallback];
if (!topLayoutGuideViewController.isViewLoaded) {
self.topLayoutGuideConstraint = nil;
return;
}
if (self.topLayoutGuideAdjustmentEnabled ||
[topLayoutGuideViewController.view.constraints count] > 0) {
self.topLayoutGuideConstraint =
[self fhv_topLayoutGuideConstraintForViewController:topLayoutGuideViewController];
}
}
- (NSLayoutConstraint *)fhv_topLayoutGuideConstraintForViewController:
(UIViewController *)viewController {
// Note: accessing topLayoutGuide has the side effect of setting up all of the view controller
// constraints. We need to access this property before we enter the for loop, otherwise
// view.constraints will be empty.
id<UILayoutSupport> topLayoutGuide = viewController.topLayoutGuide;
NSLayoutConstraint *foundConstraint = nil;
for (NSLayoutConstraint *constraint in viewController.view.constraints) {
if (constraint.firstItem == topLayoutGuide && constraint.secondItem == nil) {
foundConstraint = constraint;
}
}
return foundConstraint;
}
- (void)setTopLayoutGuideConstraint:(NSLayoutConstraint *)topLayoutGuideConstraint {
if (_topLayoutGuideConstraint == topLayoutGuideConstraint) {
return;
}
/*
On pre-iOS 11 devices, the top layout guide's constant gets set by UIKit multiple times on
call stacks that we have no influence over, meaning it's easy for our custom top layout guide
value to be lost (being reset to UIKit's default of "20" usually). We want to have final say on
the value though, so we KVO the property and re-apply our calculated top layout guide any time
the top layout guide is modified.
We only do this on pre-iOS 11 devices because iOS 11 and above are less aggressive about setting
the top layout guide constant (and clients should be relying on additionalSafeAreaInsets anyway).
*/
BOOL shouldObserveLayoutGuideConstant = YES;
if (@available(iOS 11.0, *)) {
shouldObserveLayoutGuideConstant = NO;
}
if (shouldObserveLayoutGuideConstant) {
[_topLayoutGuideConstraint removeObserver:self
forKeyPath:NSStringFromSelector(@selector(constant))
context:kKVOContextMDCFlexibleHeaderViewController];
}
_topLayoutGuideConstraint = topLayoutGuideConstraint;
if (shouldObserveLayoutGuideConstant) {
[_topLayoutGuideConstraint addObserver:self
forKeyPath:NSStringFromSelector(@selector(constant))
options:NSKeyValueObservingOptionNew
context:kKVOContextMDCFlexibleHeaderViewController];
}
}
- (void)setTopSafeAreaConstraint:(NSLayoutConstraint *)topSafeAreaConstraint {
if (_topSafeAreaConstraint == topSafeAreaConstraint) {
return;
}
[_topSafeAreaConstraint removeObserver:self
forKeyPath:NSStringFromSelector(@selector(constant))
context:kKVOContextMDCFlexibleHeaderViewController];
_topSafeAreaConstraint = topSafeAreaConstraint;
[_topSafeAreaConstraint addObserver:self
forKeyPath:NSStringFromSelector(@selector(constant))
options:NSKeyValueObservingOptionNew
context:kKVOContextMDCFlexibleHeaderViewController];
}
- (void)setTopSafeAreaView:(UIView *)topSafeAreaView {
if (_topSafeAreaView == topSafeAreaView) {
return;
}
[_topSafeAreaView removeObserver:self
forKeyPath:NSStringFromSelector(@selector(safeAreaInsets))
context:kKVOContextMDCFlexibleHeaderViewController];
_topSafeAreaView = topSafeAreaView;
[_topSafeAreaView addObserver:self
forKeyPath:NSStringFromSelector(@selector(safeAreaInsets))
options:NSKeyValueObservingOptionNew
context:kKVOContextMDCFlexibleHeaderViewController];
}
- (UIViewController *)fhv_topLayoutGuideViewControllerWithFallback {
UIViewController *topLayoutGuideViewController = self.topLayoutGuideViewController;
if (!topLayoutGuideViewController && !self.topLayoutGuideAdjustmentEnabled) {
topLayoutGuideViewController = self.parentViewController;
}
return topLayoutGuideViewController;
}
- (void)fhv_setTopLayoutGuideConstraintConstant:(CGFloat)constant {
self.isUpdatingTopLayoutGuide = YES;
self.topLayoutGuideConstraint.constant = constant;
self.isUpdatingTopLayoutGuide = NO;
}
- (void)updateTopLayoutGuide {
NSAssert([NSThread isMainThread], @"updateTopLayoutGuide must be called from the main thread.");
NSAssert(!self.useAdditionalSafeAreaInsetsForWebKitScrollViews ||
(self.topLayoutGuideViewController != nil),
@"If useAdditionalSafeAreaInsetsForWebKitScrollViews is enabled you must also set a"
@"topLayoutGuideViewController.");
// We observe (using KVO) the top layout guide's constant and re-invoke updateTopLayoutGuide
// whenever it changes. We also change the constant in this method. So, to avoid a recursive
// infinite loop we bail out early here if we're the ones that initiated the top layout guide
// constant change.
if (self.isUpdatingTopLayoutGuide) {
return;
}
if (!self.topLayoutGuideAdjustmentEnabled) {
// Legacy behavior
[self fhv_setTopLayoutGuideConstraintConstant:self.flexibleHeaderViewControllerHeightOffset];
return;
}
if (![self isViewLoaded]) {
return;
}
if (!self.topLayoutGuideConstraint) {
[self fhv_extractTopLayoutGuideConstraint];
}
CGFloat topInset = CGRectGetMaxY(_headerView.frame);
// Avoid excessive re-calculations.
if (self.topLayoutGuideConstraint.constant != topInset) {
[self fhv_setTopLayoutGuideConstraintConstant:topInset];
}
if (@available(iOS 11.0, *)) {
BOOL alwaysUseAdditionalSafeAreaInsets = NO;
if (self.useAdditionalSafeAreaInsetsForWebKitScrollViews &&
[self.headerView trackingScrollViewIsWebKit]) {
alwaysUseAdditionalSafeAreaInsets = YES;
}
UIViewController *topLayoutGuideViewController =
[self fhv_topLayoutGuideViewControllerWithFallback];
// If there is a tracking scroll view then the flexible header will manage safe area insets via
// the tracking scroll view's contentInsets. Some day - in the long distant future when we only
// support iOS 11 and up - we can probably drop the content inset adjustment behavior in favor
// of modifying additionalSafeAreaInsets instead.
if (self.headerView.trackingScrollView != nil && !alwaysUseAdditionalSafeAreaInsets) {
// Reset the additional safe area insets if we are now tracking a scroll view.
if (topLayoutGuideViewController != nil) {
UIEdgeInsets additionalSafeAreaInsets =
topLayoutGuideViewController.additionalSafeAreaInsets;
additionalSafeAreaInsets.top = 0;
topLayoutGuideViewController.additionalSafeAreaInsets = additionalSafeAreaInsets;
}
} else if (topLayoutGuideViewController != nil) {
UIEdgeInsets additionalSafeAreaInsets = topLayoutGuideViewController.additionalSafeAreaInsets;
// We need to avoid double-counting any top safe area amount because this will already be
// taken into account as part of additionalSafeAreaInsets.
topInset -= self.headerView.topSafeAreaGuideHeight;
if (self.headerView.minMaxHeightIncludesSafeArea) {
topInset = MIN(self.headerView.maximumHeight - MDCDeviceTopSafeAreaInset(), topInset);
} else {
topInset = MIN(self.headerView.maximumHeight, topInset);
}
additionalSafeAreaInsets.top = topInset;
topLayoutGuideViewController.additionalSafeAreaInsets = additionalSafeAreaInsets;
}
}
}
- (CGFloat)headerViewControllerHeight {
BOOL shiftEnabledForStatusBar =
_headerView.shiftBehavior == MDCFlexibleHeaderShiftBehaviorEnabledWithStatusBar;
CGFloat statusBarHeight = [UIApplication mdc_safeSharedApplication].statusBarFrame.size.height;
CGFloat height = MAX(_headerView.frame.origin.y + _headerView.frame.size.height,
shiftEnabledForStatusBar ? 0 : statusBarHeight);
return height;
}
- (void)setTopLayoutGuideViewController:(UIViewController *)topLayoutGuideViewController {
if (topLayoutGuideViewController == _topLayoutGuideViewController) {
return;
}
_topLayoutGuideViewController = topLayoutGuideViewController;
_topLayoutGuideAdjustmentEnabled = YES;
if (self.inferTopSafeAreaInsetFromViewController) {
// Need to re-infer the top safe area source view controller because it may now be a child of
// the top layout guide view controller.
[self fhv_inferTopSafeAreaSourceViewController];
}
if ([self isViewLoaded]) {
[self fhv_extractTopLayoutGuideConstraint];
[self updateTopLayoutGuide];
}
}
- (void)setInferTopSafeAreaInsetFromViewController:(BOOL)inferTopSafeAreaInsetFromViewController {
_headerView.inferTopSafeAreaInsetFromViewController = inferTopSafeAreaInsetFromViewController;
[self fhv_inferTopSafeAreaSourceViewController];
}
- (BOOL)inferTopSafeAreaInsetFromViewController {
return _headerView.inferTopSafeAreaInsetFromViewController;
}
- (void)setUseAdditionalSafeAreaInsetsForWebKitScrollViews:
(BOOL)useAdditionalSafeAreaInsetsForWebKitScrollViews {
_headerView.useAdditionalSafeAreaInsetsForWebKitScrollViews =
useAdditionalSafeAreaInsetsForWebKitScrollViews;
}
- (BOOL)useAdditionalSafeAreaInsetsForWebKitScrollViews {
return _headerView.useAdditionalSafeAreaInsetsForWebKitScrollViews;
}
- (void)setPermitInferringTopSafeAreaFromTopLayoutGuideViewController:
(BOOL)permitInferringTopSafeAreaFromTopLayoutGuideViewController {
if (@available(iOS 11, *)) {
_permitInferringTopSafeAreaFromTopLayoutGuideViewController =
permitInferringTopSafeAreaFromTopLayoutGuideViewController;
[self fhv_inferTopSafeAreaSourceViewController];
} else {
NSAssert(
NO,
@"permitInferringTopSafeAreaFromTopLayoutGuideViewController is only supported on iOS 11+");
}
}
#pragma mark - Top safe area inset extraction
- (BOOL)fhv_isViewControllerDescendantOfTopLayoutGuideViewController:(UIViewController *)child {
// No need to walk the ancestry.
if (self.topLayoutGuideViewController == nil) {
return NO;
}
UIViewController *ancestorIterator = child;
while (ancestorIterator != nil) {
if (ancestorIterator == self.topLayoutGuideViewController) {
return YES;
}
ancestorIterator = ancestorIterator.parentViewController;
}
return NO;
}
- (UIViewController *)fhv_rootAncestorOfViewController:(UIViewController *)viewController {
while (viewController.parentViewController != nil) {
viewController = viewController.parentViewController;
}
return viewController;
}
// This method makes the assumption that the root-most view controller is the best view controller
// to look at when determining the top safe area inset. It's possible that this assumption will not
// hold true in all cases, so we may also need to expose an explicit API for setting the top safe
// area source view controller.
- (void)fhv_inferTopSafeAreaSourceViewController {
UIViewController *parent = self.parentViewController;
if (parent == nil || !self.inferTopSafeAreaInsetFromViewController) {
_headerView.topSafeAreaSourceViewController = nil;
self.topSafeAreaConstraint = nil;
self.topSafeAreaView = nil;
return;
}
UIViewController *ancestor =
[self.safeAreaDelegate flexibleHeaderViewControllerTopSafeAreaInsetViewController:self];
if (ancestor == nil) {
ancestor = [self fhv_rootAncestorOfViewController:parent];
// Use instance variable here instead of property, as property is marked as available only on
// iOS 11+.
if (_permitInferringTopSafeAreaFromTopLayoutGuideViewController) {
// On iOS 11, we can subtract the additional safe area insets to find the correct value.
} else {
// Are we attempting to extract the top safe area inset from our top layout guide view
// controller?
if (self.topLayoutGuideAdjustmentEnabled && ancestor == self.topLayoutGuideViewController) {
// We can't use the provided ancestor because it's a child of the top layout guide view
// controller. Doing so would result in the top layout guide being infinitely increased.
ancestor = nil;
}
}
}
// if ancestor == nil at this point, then we're in a bad spot because there's nowhere for us to
// extract a top safe area inset from. Should we throw an assert?
NSAssert(ancestor != nil,
@"inferTopSafeAreaInsetFromViewController is true but we were unable to infer a view "
@"controller from which we could extract a safe area. Consider placing your view "
@"controller inside a container view controller.");
if (_headerView.topSafeAreaSourceViewController != ancestor) {
_headerView.topSafeAreaSourceViewController = ancestor;
_headerView.subtractsAdditionalSafeAreaInsets =
// Use instance variable here instead of property, as property is marked as available only
// on iOS 11+.
_permitInferringTopSafeAreaFromTopLayoutGuideViewController &&
self.topLayoutGuideAdjustmentEnabled && ancestor == self.topLayoutGuideViewController;
BOOL shouldObserveLayoutGuide = YES;
if (@available(iOS 11.0, *)) {
shouldObserveLayoutGuide = NO;
}
if (shouldObserveLayoutGuide) {
self.topSafeAreaConstraint = [self fhv_topLayoutGuideConstraintForViewController:ancestor];
} else {
self.topSafeAreaView = ancestor.view;
}
}
}
#pragma mark MDCFlexibleHeaderViewDelegate
- (void)flexibleHeaderViewNeedsStatusBarAppearanceUpdate:
(__unused MDCFlexibleHeaderView *)headerView {
[self setNeedsStatusBarAppearanceUpdate];
}
- (void)flexibleHeaderViewFrameDidChange:(MDCFlexibleHeaderView *)headerView {
if (self.topLayoutGuideAdjustmentEnabled) {
[self updateTopLayoutGuide];
} else {
// Legacy behavior
// Whenever the flexibleHeaderView's frame changes, we update the value of the height offset
self.flexibleHeaderViewControllerHeightOffset = [self headerViewControllerHeight];
// We must change the constant of the constraint attached to our parentViewController's
// topLayoutGuide to trigger the re-layout of its subviews
[self fhv_setTopLayoutGuideConstraintConstant:self.flexibleHeaderViewControllerHeightOffset];
}
[self.layoutDelegate flexibleHeaderViewController:self
flexibleHeaderViewFrameDidChange:headerView];
}
#pragma mark - Hairline support
- (void)setShowsHairline:(BOOL)showsHairline {
self.hairline.hidden = !showsHairline;
}
- (BOOL)showsHairline {
return !self.hairline.hidden;
}
- (void)setHairlineColor:(UIColor *)hairlineColor {
self.hairline.color = hairlineColor;
}
- (UIColor *)hairlineColor {
return self.hairline.color;
}
@end