blob: c843c79672b25572993995048ad91a681e699b98 [file] [log] [blame] [edit]
// Copyright 2016-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 "MDCAppBarViewController.h"
#import "MDCAppBarContainerViewController.h"
#import "private/MaterialAppBarStrings.h"
#import "private/MaterialAppBarStrings_table.h"
#import "MDCAppBarViewControllerAccessibilityPerformEscapeDelegate.h"
#import "MaterialFlexibleHeader.h"
#import "MaterialHeaderStackView.h"
#import "MaterialNavigationBar.h"
#import "MaterialShadowElevations.h"
#import "MaterialShadowLayer.h"
#import "MaterialIcons+ic_arrow_back.h"
#import "MaterialUIMetrics.h"
#import <MDFInternationalization/MDFInternationalization.h>
static NSString *const kBarStackKey = @"barStack";
// The Bundle for string resources.
static NSString *const kMaterialAppBarBundle = @"MaterialAppBar.bundle";
@implementation MDCAppBarViewController {
NSLayoutConstraint *_verticalConstraint;
NSLayoutConstraint *_topSafeAreaConstraint;
}
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
[self MDCAppBarViewController_commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self MDCAppBarViewController_commonInit];
}
return self;
}
- (void)MDCAppBarViewController_commonInit {
// Shadow layer
__weak MDCAppBarViewController *weakSelf = self;
MDCFlexibleHeaderShadowIntensityChangeBlock intensityBlock =
^(CALayer *_Nonnull shadowLayer, CGFloat intensity) {
CGFloat elevation = MDCShadowElevationAppBar * intensity;
weakSelf.headerView.elevation = elevation;
[(MDCShadowLayer *)shadowLayer setElevation:elevation];
};
[self.headerView setShadowLayer:[MDCShadowLayer layer] intensityDidChangeBlock:intensityBlock];
[self.headerView forwardTouchEventsForView:self.headerStackView];
[self.headerView forwardTouchEventsForView:self.navigationBar];
self.headerStackView.translatesAutoresizingMaskIntoConstraints = NO;
self.headerStackView.topBar = self.navigationBar;
}
#pragma mark - Properties
- (MDCHeaderStackView *)headerStackView {
// Removed call to loadView here as we should never be calling it manually.
// It previously replaced loadViewIfNeeded call that is only iOS 9.0+ to
// make backwards compatible.
// Underlying issue is you need view loaded before accessing. Below change will accomplish that
// by calling for view.bounds initializing the stack view
if (!_headerStackView) {
_headerStackView = [[MDCHeaderStackView alloc] initWithFrame:CGRectZero];
}
return _headerStackView;
}
- (MDCNavigationBar *)navigationBar {
if (!_navigationBar) {
_navigationBar = [[MDCNavigationBar alloc] init];
}
return _navigationBar;
}
- (UIBarButtonItem *)backButtonItem {
UIViewController *parent = self.parentViewController;
UINavigationController *navigationController = parent.navigationController;
NSArray<UIViewController *> *viewControllerStack = navigationController.viewControllers;
// This will be zero if there is no navigation controller, so a view controller which is not
// inside a navigation controller will be treated the same as a view controller at the root of a
// navigation controller
NSUInteger index = [viewControllerStack indexOfObject:parent];
UIViewController *iterator = parent;
// In complex cases it might actually be a parent of @c fhvParent which is on the nav stack.
while (index == NSNotFound && iterator && ![iterator isEqual:navigationController]) {
iterator = iterator.parentViewController;
index = [viewControllerStack indexOfObject:iterator];
}
if (index == NSNotFound) {
NSCAssert(NO, @"View controller not present in its own navigation controller.");
// This is not something which should ever happen, but just in case.
return nil;
}
if (index == 0) {
// The view controller is at the root of a navigation stack (or not in one).
return nil;
}
UIViewController *previousViewControler = navigationController.viewControllers[index - 1];
if ([previousViewControler isKindOfClass:[MDCAppBarContainerViewController class]]) {
// Special case: if the previous view controller is a container controller, use its content
// view controller.
MDCAppBarContainerViewController *chvc =
(MDCAppBarContainerViewController *)previousViewControler;
previousViewControler = chvc.contentViewController;
}
UIBarButtonItem *backBarButtonItem = previousViewControler.navigationItem.backBarButtonItem;
if (!backBarButtonItem) {
UIImage *backButtonImage = [MDCIcons imageFor_ic_arrow_back];
backButtonImage = [backButtonImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
if (self.navigationBar.mdf_effectiveUserInterfaceLayoutDirection ==
UIUserInterfaceLayoutDirectionRightToLeft) {
backButtonImage = [backButtonImage mdf_imageWithHorizontallyFlippedOrientation];
}
backBarButtonItem = [[UIBarButtonItem alloc] initWithImage:backButtonImage
style:UIBarButtonItemStyleDone
target:self
action:@selector(didTapBackButton:)];
}
backBarButtonItem.accessibilityIdentifier = @"back_bar_button";
NSString *key = kMaterialAppBarStringTable[kStr_MaterialAppBarBackButtonAccessibilityLabel];
backBarButtonItem.accessibilityLabel = NSLocalizedStringFromTableInBundle(
key, kMaterialAppBarStringsTableName, [[self class] bundle], @"Back");
return backBarButtonItem;
}
- (void)setShouldAdjustHeightBasedOnHeaderStackView:(BOOL)shouldAdjustHeightBasedOnHeaderStackView {
_shouldAdjustHeightBasedOnHeaderStackView = shouldAdjustHeightBasedOnHeaderStackView;
if (shouldAdjustHeightBasedOnHeaderStackView) {
self.headerView.minMaxHeightIncludesSafeArea = NO;
[self adjustHeightBasedOnHeaderStackView];
}
}
- (void)setInferTopSafeAreaInsetFromViewController:(BOOL)inferTopSafeAreaInsetFromViewController {
[super setInferTopSafeAreaInsetFromViewController:inferTopSafeAreaInsetFromViewController];
if (inferTopSafeAreaInsetFromViewController) {
self.topLayoutGuideAdjustmentEnabled = YES;
}
_verticalConstraint.active = !self.inferTopSafeAreaInsetFromViewController;
_topSafeAreaConstraint.active = self.inferTopSafeAreaInsetFromViewController;
}
- (void)setHeaderStackViewOffset:(CGFloat)headerStackViewOffset {
_headerStackViewOffset = headerStackViewOffset;
_verticalConstraint.constant = [self verticalContraintLength];
_topSafeAreaConstraint.constant = [self topSafeAreaContraintLength];
}
#pragma mark - Resource bundle
+ (NSBundle *)bundle {
static NSBundle *bundle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
bundle = [NSBundle bundleWithPath:[self bundlePathWithName:kMaterialAppBarBundle]];
});
return bundle;
}
+ (NSString *)bundlePathWithName:(NSString *)bundleName {
// In iOS 8+, we could be included by way of a dynamic framework, and our resource bundles may
// not be in the main .app bundle, but rather in a nested framework, so figure out where we live
// and use that as the search location.
NSBundle *bundle = [NSBundle bundleForClass:[MDCAppBar class]];
NSString *resourcePath = [(nil == bundle ? [NSBundle mainBundle] : bundle) resourcePath];
return [resourcePath stringByAppendingPathComponent:bundleName];
}
#pragma mark - UIViewController Overrides
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.headerStackView];
// Bar stack expands vertically, but has a margin above it for the status bar.
NSArray<NSLayoutConstraint *> *horizontalConstraints = [NSLayoutConstraint
constraintsWithVisualFormat:[NSString stringWithFormat:@"H:|[%@]|", kBarStackKey]
options:0
metrics:nil
views:@{kBarStackKey : self.headerStackView}];
[self.view addConstraints:horizontalConstraints];
CGFloat topMargin = [self verticalContraintLength];
_verticalConstraint = [NSLayoutConstraint constraintWithItem:self.headerStackView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeTop
multiplier:1
constant:topMargin];
_verticalConstraint.active = !self.inferTopSafeAreaInsetFromViewController;
_topSafeAreaConstraint =
[NSLayoutConstraint constraintWithItem:self.headerStackView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.headerView.topSafeAreaGuide
attribute:NSLayoutAttributeBottom
multiplier:1
constant:[self topSafeAreaContraintLength]];
_topSafeAreaConstraint.active = self.inferTopSafeAreaInsetFromViewController;
[NSLayoutConstraint constraintWithItem:self.headerStackView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1
constant:0]
.active = YES;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
UIBarButtonItem *backBarButtonItem = [self backButtonItem];
if (backBarButtonItem && !self.navigationBar.backItem) {
self.navigationBar.backItem = backBarButtonItem;
}
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
if (@available(iOS 11.0, *)) {
// We only update the top inset on iOS 11 because previously we were not adjusting the header
// height to make it smaller when the status bar is hidden.
_verticalConstraint.constant = [self verticalContraintLength];
}
if (self.shouldAdjustHeightBasedOnHeaderStackView) {
[self adjustHeightBasedOnHeaderStackView];
}
}
- (void)didMoveToParentViewController:(UIViewController *)parent {
[super didMoveToParentViewController:parent];
[self.navigationBar observeNavigationItem:parent.navigationItem];
CGRect frame = self.view.frame;
frame.size.width = CGRectGetWidth(parent.view.bounds);
self.view.frame = frame;
}
#pragma mark - UIAccessibility
- (BOOL)accessibilityPerformEscape {
if (self.accessibilityPerformEscapeDelegate) {
return [self.accessibilityPerformEscapeDelegate
appBarViewControllerAccessibilityPerformEscape:self];
}
// Fall-back behavior.
[self dismissSelf];
return YES;
}
#pragma mark User actions
- (void)didTapBackButton:(__unused id)sender {
[self dismissSelf];
}
- (void)dismissSelf {
UIViewController *pvc = self.parentViewController;
if (pvc.navigationController && pvc.navigationController.viewControllers.count > 1) {
[pvc.navigationController popViewControllerAnimated:YES];
} else {
[pvc dismissViewControllerAnimated:YES completion:nil];
}
}
#pragma mark - Private
- (CGFloat)verticalContraintLength {
return MDCDeviceTopSafeAreaInset() + _headerStackViewOffset;
}
- (CGFloat)topSafeAreaContraintLength {
return _headerStackViewOffset;
}
- (void)adjustHeightBasedOnHeaderStackView {
CGFloat heightSum = 0;
heightSum += [self.headerStackView.topBar sizeThatFits:self.view.bounds.size].height;
heightSum += [self.headerStackView.bottomBar sizeThatFits:self.view.bounds.size].height;
heightSum += _headerStackViewOffset;
self.headerView.minimumHeight = heightSum;
self.headerView.maximumHeight = heightSum;
}
@end
#pragma mark - To be deprecated
@implementation MDCAppBar
- (instancetype)init {
self = [super init];
if (self) {
_appBarViewController = [[MDCAppBarViewController alloc] init];
}
return self;
}
- (MDCFlexibleHeaderViewController *)headerViewController {
return self.appBarViewController;
}
- (MDCHeaderStackView *)headerStackView {
return self.appBarViewController.headerStackView;
}
- (MDCNavigationBar *)navigationBar {
return self.appBarViewController.navigationBar;
}
- (void)addSubviewsToParent {
MDCAppBarViewController *abvc = self.appBarViewController;
NSAssert(abvc.parentViewController,
@"appBarViewController does not have a parentViewController. "
@"Use [self addChildViewController:appBar.appBarViewController]. "
@"This warning only appears in DEBUG builds");
if (abvc.view.superview == abvc.parentViewController.view) {
return;
}
// Enforce the header's desire to fully cover the width of its parent view.
CGRect frame = abvc.view.frame;
frame.origin.x = 0;
frame.size.width = abvc.parentViewController.view.bounds.size.width;
abvc.view.frame = frame;
[abvc.parentViewController.view addSubview:abvc.view];
[abvc didMoveToParentViewController:abvc.parentViewController];
}
- (void)setInferTopSafeAreaInsetFromViewController:(BOOL)inferTopSafeAreaInsetFromViewController {
self.appBarViewController.inferTopSafeAreaInsetFromViewController =
inferTopSafeAreaInsetFromViewController;
}
- (BOOL)inferTopSafeAreaInsetFromViewController {
return self.appBarViewController.inferTopSafeAreaInsetFromViewController;
}
@end