| /* |
| 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 "MDCAppBar.h" |
| |
| #import "MDCAppBarContainerViewController.h" |
| |
| #import "MaterialFlexibleHeader.h" |
| #import "MaterialIcons+ic_arrow_back.h" |
| #import "MaterialRTL.h" |
| #import "MaterialShadowElevations.h" |
| #import "MaterialShadowLayer.h" |
| #import "MaterialTypography.h" |
| #import "private/MaterialAppBarStrings.h" |
| #import "private/MaterialAppBarStrings_table.h" |
| |
| static NSString *const kBarStackKey = @"barStack"; |
| static NSString *const kStatusBarHeightKey = @"statusBarHeight"; |
| static NSString *const MDCAppBarHeaderViewControllerKey = @"MDCAppBarHeaderViewControllerKey"; |
| static NSString *const MDCAppBarNavigationBarKey = @"MDCAppBarNavigationBarKey"; |
| static NSString *const MDCAppBarHeaderStackViewKey = @"MDCAppBarHeaderStackViewKey"; |
| static const CGFloat kStatusBarHeight = 20; |
| |
| // The Bundle for string resources. |
| static NSString *const kMaterialAppBarBundle = @"MaterialAppBar.bundle"; |
| |
| @class MDCAppBarViewController; |
| |
| @interface MDCAppBar () |
| |
| @property(nonatomic, strong) MDCAppBarViewController *appBarController; |
| |
| @end |
| |
| @interface MDCAppBarViewController : UIViewController |
| |
| @property(nonatomic, strong) MDCHeaderStackView *headerStackView; |
| @property(nonatomic, strong) MDCNavigationBar *navigationBar; |
| |
| @end |
| |
| @implementation MDCAppBar |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| [self commonMDCAppBarInit]; |
| [self commonMDCAppBarViewSetup]; |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithCoder:(NSCoder *)aDecoder { |
| self = [super init]; |
| if (self) { |
| [self commonMDCAppBarInit]; |
| if ([aDecoder containsValueForKey:MDCAppBarHeaderViewControllerKey]) { |
| _headerViewController = [aDecoder decodeObjectForKey:MDCAppBarHeaderViewControllerKey]; |
| } |
| |
| if ([aDecoder containsValueForKey:MDCAppBarNavigationBarKey]) { |
| _navigationBar = [aDecoder decodeObjectForKey:MDCAppBarNavigationBarKey]; |
| _appBarController.navigationBar = _navigationBar; |
| } |
| |
| if ([aDecoder containsValueForKey:MDCAppBarHeaderStackViewKey]) { |
| _headerStackView = [aDecoder decodeObjectForKey:MDCAppBarHeaderStackViewKey]; |
| _appBarController.headerStackView = _headerStackView; |
| } |
| |
| [self commonMDCAppBarViewSetup]; |
| } |
| return self; |
| } |
| |
| - (void)encodeWithCoder:(NSCoder *)aCoder { |
| [aCoder encodeObject:self.headerViewController forKey:MDCAppBarHeaderViewControllerKey]; |
| [aCoder encodeObject:self.navigationBar forKey:MDCAppBarNavigationBarKey]; |
| [aCoder encodeObject:self.headerStackView forKey:MDCAppBarHeaderStackViewKey]; |
| } |
| |
| - (void)commonMDCAppBarInit { |
| _headerViewController = [[MDCFlexibleHeaderViewController alloc] init]; |
| |
| // Shadow layer |
| MDCFlexibleHeaderView *headerView = _headerViewController.headerView; |
| MDCFlexibleHeaderShadowIntensityChangeBlock intensityBlock = |
| ^(CALayer *_Nonnull shadowLayer, CGFloat intensity) { |
| CGFloat elevation = MDCShadowElevationAppBar * intensity; |
| [(MDCShadowLayer *)shadowLayer setElevation:elevation]; |
| }; |
| [headerView setShadowLayer:[MDCShadowLayer layer] intensityDidChangeBlock:intensityBlock]; |
| _appBarController = [[MDCAppBarViewController alloc] init]; |
| _headerStackView = _appBarController.headerStackView; |
| _navigationBar = _appBarController.navigationBar; |
| } |
| |
| - (void)commonMDCAppBarViewSetup { |
| [_headerViewController addChildViewController:_appBarController]; |
| _appBarController.view.frame = _headerViewController.view.bounds; |
| [_headerViewController.view addSubview:_appBarController.view]; |
| [_appBarController didMoveToParentViewController:_headerViewController]; |
| |
| [_headerViewController.headerView forwardTouchEventsForView:_appBarController.headerStackView]; |
| [_headerViewController.headerView forwardTouchEventsForView:_appBarController.navigationBar]; |
| } |
| |
| - (void)addHeaderViewControllerToParentViewController: |
| (nonnull UIViewController *)parentViewController { |
| [parentViewController addChildViewController:_headerViewController]; |
| } |
| |
| - (void)addSubviewsToParent { |
| MDCFlexibleHeaderViewController *fhvc = self.headerViewController; |
| NSAssert(fhvc.parentViewController, |
| @"headerViewController does not have a parentViewController. " |
| @"Use [self addChildViewController:appBar.headerViewController]. " |
| @"This warning only appears in DEBUG builds"); |
| if (fhvc.view.superview == fhvc.parentViewController.view) { |
| return; |
| } |
| |
| // Enforce the header's desire to fully cover the width of its parent view. |
| CGRect frame = fhvc.view.frame; |
| frame.origin.x = 0; |
| frame.size.width = fhvc.parentViewController.view.bounds.size.width; |
| fhvc.view.frame = frame; |
| |
| [fhvc.parentViewController.view addSubview:fhvc.view]; |
| [fhvc didMoveToParentViewController:fhvc.parentViewController]; |
| |
| [self.navigationBar observeNavigationItem:fhvc.parentViewController.navigationItem]; |
| } |
| |
| @end |
| |
| @implementation MDCAppBarViewController |
| |
| - (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; |
| } |
| |
| - (UIViewController *)flexibleHeaderParentViewController { |
| NSAssert([self.parentViewController isKindOfClass:[MDCFlexibleHeaderViewController class]], |
| @"Expected the parent of %@ to be a type of %@", NSStringFromClass([self class]), |
| NSStringFromClass([MDCFlexibleHeaderViewController class])); |
| return self.parentViewController.parentViewController; |
| } |
| |
| - (UIBarButtonItem *)backButtonItem { |
| UIViewController *fhvParent = self.flexibleHeaderParentViewController; |
| UINavigationController *navigationController = fhvParent.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:fhvParent]; |
| |
| UIViewController *iterator = fhvParent; |
| |
| // 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 = [UIImage imageWithContentsOfFile:[MDCIcons pathFor_ic_arrow_back]]; |
| backButtonImage = [backButtonImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; |
| if (self.navigationBar.mdc_effectiveUserInterfaceLayoutDirection == |
| UIUserInterfaceLayoutDirectionRightToLeft) { |
| backButtonImage = [backButtonImage mdc_imageFlippedForRightToLeftLayoutDirection]; |
| } |
| 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; |
| } |
| |
| #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:[self class]]; |
| NSString *resourcePath = [(nil == bundle ? [NSBundle mainBundle] : bundle)resourcePath]; |
| return [resourcePath stringByAppendingPathComponent:bundleName]; |
| } |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| |
| self.headerStackView.translatesAutoresizingMaskIntoConstraints = NO; |
| self.headerStackView.topBar = self.navigationBar; |
| |
| [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]; |
| |
| NSArray<NSLayoutConstraint *> *verticalConstraints = [NSLayoutConstraint |
| constraintsWithVisualFormat:[NSString stringWithFormat:@"V:|-%@-[%@]|", kStatusBarHeightKey, |
| kBarStackKey] |
| options:0 |
| metrics:@{ |
| kStatusBarHeightKey : @(kStatusBarHeight) |
| } |
| views:@{kBarStackKey : self.headerStackView}]; |
| [self.view addConstraints:verticalConstraints]; |
| } |
| |
| - (void)viewWillAppear:(BOOL)animated { |
| [super viewWillAppear:animated]; |
| |
| UIBarButtonItem *backBarButtonItem = [self backButtonItem]; |
| if (backBarButtonItem && !self.navigationBar.backItem) { |
| self.navigationBar.backItem = backBarButtonItem; |
| } |
| } |
| |
| #pragma mark User actions |
| |
| - (void)didTapBackButton:(id)sender { |
| UIViewController *pvc = self.flexibleHeaderParentViewController; |
| if (pvc.navigationController && pvc.navigationController.viewControllers.count > 1) { |
| [pvc.navigationController popViewControllerAnimated:YES]; |
| } else { |
| [pvc dismissViewControllerAnimated:YES completion:nil]; |
| } |
| } |
| |
| @end |