| // 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 |