| // Copyright 2018-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 "MDCBottomNavigationBarController.h" |
| |
| // A context for Key Value Observing |
| static void *const kObservationContext = (void *)&kObservationContext; |
| |
| @interface MDCBottomNavigationBarController () |
| |
| /** The view that hosts the content for the selected view controller **/ |
| @property(nonatomic, strong) UIView *content; |
| |
| @end |
| |
| @implementation MDCBottomNavigationBarController |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| _navigationBar = [[MDCBottomNavigationBar alloc] init]; |
| _content = [[UIView alloc] init]; |
| _viewControllers = @[]; |
| _selectedIndex = NSNotFound; |
| |
| [_navigationBar addObserver:self |
| forKeyPath:NSStringFromSelector(@selector(items)) |
| options:NSKeyValueObservingOptionNew |
| context:kObservationContext]; |
| } |
| |
| return self; |
| } |
| |
| - (void)dealloc { |
| [_navigationBar removeObserver:self forKeyPath:NSStringFromSelector(@selector(items))]; |
| } |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| |
| self.navigationBar.delegate = self; |
| |
| // Add subviews and create their constraints |
| [self.view addSubview:self.content]; |
| [self.view addSubview:self.navigationBar]; |
| [self loadConstraints]; |
| } |
| |
| - (void)setSelectedViewController:(nullable UIViewController *)selectedViewController { |
| // Assert that the given VC is one of our view controllers or it is nil (we are unselecting) |
| NSAssert( |
| selectedViewController == nil || [self.viewControllers containsObject:selectedViewController], |
| @"Attempting to set BottomBarViewControllers to a view controller it does not contain"); |
| |
| // Early return if we are already set to the given VC |
| if (self.selectedViewController == selectedViewController) { |
| return; |
| } |
| |
| // Remove current VC and add new one. |
| [self removeContentViewController:self.selectedViewController]; |
| [self addContentViewController:selectedViewController]; |
| |
| // Set the iVar and update selected index |
| _selectedViewController = selectedViewController; |
| self.selectedIndex = [self.viewControllers indexOfObject:selectedViewController]; |
| } |
| |
| - (void)setSelectedIndex:(NSUInteger)selectedIndex { |
| // If we are setting to NSNotFound deselect the items |
| if (selectedIndex == NSNotFound) { |
| [self deselectCurrentItem]; |
| return; |
| } |
| |
| BOOL outOfBounds = selectedIndex >= self.viewControllers.count || |
| selectedIndex >= self.navigationBar.items.count; |
| |
| NSAssert(!outOfBounds, |
| @"Attempting to set BottomBarViewController's selectedIndex to %li. This" |
| " value is not within the bounds of the navigation bar's items and/or view controllers", |
| (unsigned long)selectedIndex); |
| |
| // Early return if we are out of bounds or if the the index is already selected. |
| if (outOfBounds || selectedIndex == _selectedIndex) { |
| return; |
| } |
| |
| // Update the selected index value and views. |
| _selectedIndex = selectedIndex; |
| [self updateViewsForSelectedIndex:selectedIndex]; |
| } |
| |
| - (void)setViewControllers:(NSArray<UIViewController *> *)viewControllers { |
| [self deselectCurrentItem]; |
| NSArray *viewControllersCopy = [viewControllers copy]; |
| _viewControllers = viewControllersCopy; |
| self.navigationBar.items = [self tabBarItemsForViewControllers:viewControllersCopy]; |
| |
| self.selectedViewController = viewControllersCopy.firstObject; |
| } |
| |
| - (UIViewController *)childViewControllerForStatusBarStyle { |
| return self.selectedViewController; |
| } |
| |
| - (UIViewController *)childViewControllerForStatusBarHidden { |
| return self.selectedViewController; |
| } |
| |
| #pragma mark - MDCBottomNavigationBarDelegate |
| |
| - (void)bottomNavigationBar:(MDCBottomNavigationBar *)bottomNavigationBar |
| didSelectItem:(UITabBarItem *)item { |
| // Early return if we cannot find the view controller. |
| NSUInteger index = [self.navigationBar.items indexOfObject:item]; |
| if (index >= [self.viewControllers count] || index == NSNotFound) { |
| return; |
| } |
| |
| // Update selected view controller |
| UIViewController *selectedViewController = [self.viewControllers objectAtIndex:index]; |
| self.selectedViewController = selectedViewController; |
| |
| // Notify the delegate. |
| if ([self.delegate respondsToSelector:@selector(bottomNavigationBarController: |
| didSelectViewController:)]) { |
| [self.delegate bottomNavigationBarController:self |
| didSelectViewController:selectedViewController]; |
| } |
| } |
| |
| - (BOOL)bottomNavigationBar:(MDCBottomNavigationBar *)bottomNavigationBar |
| shouldSelectItem:(UITabBarItem *)item { |
| NSUInteger index = [self.navigationBar.items indexOfObject:item]; |
| if (index >= [self.viewControllers count] || index == NSNotFound) { |
| return NO; |
| } |
| |
| // Pass the response to the delegate if they want to handle this request. |
| if ([self.delegate respondsToSelector:@selector(bottomNavigationBarController: |
| didSelectViewController:)]) { |
| UIViewController *viewControllerToSelect = [self.viewControllers objectAtIndex:index]; |
| return [self.delegate bottomNavigationBarController:self |
| shouldSelectViewController:viewControllerToSelect]; |
| } |
| |
| return YES; |
| } |
| |
| #pragma mark - Key Value Observation Methods |
| |
| - (void)observeValueForKeyPath:(NSString *)keyPath |
| ofObject:(id)object |
| change:(NSDictionary<NSKeyValueChangeKey, id> *)change |
| context:(void *)context { |
| if (context != kObservationContext) { |
| [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; |
| return; |
| } |
| |
| id newValue = [change objectForKey:NSKeyValueChangeNewKey]; |
| if (object == self.navigationBar && |
| [keyPath isEqualToString:NSStringFromSelector(@selector(items))] && |
| [newValue isKindOfClass:[NSArray class]]) { |
| [self didUpdateNavigationBarItemsWithNewValue:(NSArray *)newValue]; |
| } |
| } |
| |
| - (void)didUpdateNavigationBarItemsWithNewValue:(NSArray *)items { |
| // Verify tab bar items correspond with the view controllers tab bar items. |
| if (items.count != self.viewControllers.count) { |
| [[self unauthorizedItemsChangedException] raise]; |
| } |
| |
| // Verify each new and the view controller's tab bar items are equal. |
| for (NSUInteger i = 0; i < self.viewControllers.count; i++) { |
| UITabBarItem *viewControllerTabBarItem = [self.viewControllers objectAtIndex:i].tabBarItem; |
| UITabBarItem *newTabBarItem = [items objectAtIndex:i]; |
| if (![viewControllerTabBarItem isEqual:newTabBarItem]) { |
| [[self unauthorizedItemsChangedException] raise]; |
| } |
| } |
| } |
| |
| #pragma mark - Private Methods |
| |
| /** |
| * Removes the given view controller from its parent view controller and its view from its superview |
| */ |
| - (void)removeContentViewController:(UIViewController *)viewController { |
| [viewController removeFromParentViewController]; |
| [viewController.view removeFromSuperview]; |
| } |
| |
| /** |
| * Adds the given view controller to the view controller hierarchy and its view to the content |
| * view. |
| */ |
| - (void)addContentViewController:(UIViewController *)viewController { |
| BOOL doesNotContainViewController = ![self.childViewControllers containsObject:viewController]; |
| if (viewController && doesNotContainViewController) { |
| [self addChildViewController:viewController]; |
| [self.content addSubview:viewController.view]; |
| [self addConstraintsForContentView:viewController.view]; |
| [viewController didMoveToParentViewController:self]; |
| } |
| } |
| |
| /** |
| * Deselects the currently set item. Sets the selectedIndex to NSNotFound, the naviagation bar's |
| * selected item to nil, and the selectedViewController to nil. |
| */ |
| - (void)deselectCurrentItem { |
| _selectedIndex = NSNotFound; |
| self.navigationBar.selectedItem = nil; |
| |
| // Force removal of the currently selected viewcontroller if there is one. |
| self.selectedViewController = nil; |
| } |
| |
| /** |
| * Sets the selected view controller to the corresponding index and updates the navigation bar's |
| * selected item. |
| */ |
| - (void)updateViewsForSelectedIndex:(NSUInteger)index { |
| // Update the selected view controller |
| UIViewController *selectedViewController = [self.viewControllers objectAtIndex:index]; |
| self.selectedViewController = selectedViewController; |
| |
| // Update the navigation bar's selected item. |
| self.navigationBar.selectedItem = selectedViewController.tabBarItem; |
| [self setNeedsStatusBarAppearanceUpdate]; |
| } |
| |
| /** |
| * Hooks up the constraints for the subviews of this controller. Namely the content view and the |
| * navigation bar. |
| */ |
| - (void)loadConstraints { |
| self.content.translatesAutoresizingMaskIntoConstraints = NO; |
| self.navigationBar.translatesAutoresizingMaskIntoConstraints = NO; |
| |
| if (@available(iOS 9.0, *)) { |
| [self loadiOS9PlusConstraints]; |
| } else { |
| [self loadPreiOS9Constraints]; |
| } |
| } |
| |
| - (void)loadPreiOS9Constraints { |
| // Navigation Bar Constraints |
| NSArray<NSLayoutConstraint *> *navigationBarConstraints = @[ |
| [NSLayoutConstraint constraintWithItem:self.navigationBar |
| attribute:NSLayoutAttributeLeading |
| relatedBy:NSLayoutRelationEqual |
| toItem:self.view |
| attribute:NSLayoutAttributeLeading |
| multiplier:1 |
| constant:0], |
| [NSLayoutConstraint constraintWithItem:self.navigationBar |
| attribute:NSLayoutAttributeTrailing |
| relatedBy:NSLayoutRelationEqual |
| toItem:self.view |
| attribute:NSLayoutAttributeTrailing |
| multiplier:1 |
| constant:0], |
| [NSLayoutConstraint constraintWithItem:self.navigationBar |
| attribute:NSLayoutAttributeBottom |
| relatedBy:NSLayoutRelationEqual |
| toItem:self.view |
| attribute:NSLayoutAttributeBottom |
| multiplier:1 |
| constant:0], |
| [NSLayoutConstraint constraintWithItem:self.navigationBar |
| attribute:NSLayoutAttributeTop |
| relatedBy:NSLayoutRelationEqual |
| toItem:self.content |
| attribute:NSLayoutAttributeBottom |
| multiplier:1 |
| constant:0] |
| ]; |
| |
| // Content View Constraints |
| NSArray<NSLayoutConstraint *> *contentConstraints = @[ |
| [NSLayoutConstraint constraintWithItem:self.content |
| attribute:NSLayoutAttributeLeading |
| relatedBy:NSLayoutRelationEqual |
| toItem:self.view |
| attribute:NSLayoutAttributeLeading |
| multiplier:1 |
| constant:0], |
| [NSLayoutConstraint constraintWithItem:self.content |
| attribute:NSLayoutAttributeTrailing |
| relatedBy:NSLayoutRelationEqual |
| toItem:self.view |
| attribute:NSLayoutAttributeTrailing |
| multiplier:1 |
| constant:0], |
| [NSLayoutConstraint constraintWithItem:self.content |
| attribute:NSLayoutAttributeTop |
| relatedBy:NSLayoutRelationEqual |
| toItem:self.view |
| attribute:NSLayoutAttributeTop |
| multiplier:1 |
| constant:0] |
| ]; |
| |
| [NSLayoutConstraint activateConstraints:navigationBarConstraints]; |
| [NSLayoutConstraint activateConstraints:contentConstraints]; |
| } |
| |
| - (void)loadiOS9PlusConstraints { |
| if (@available(iOS 9.0, *)) { |
| // Navigation Bar Constraints |
| [self.view.leftAnchor constraintEqualToAnchor:self.navigationBar.leftAnchor].active = YES; |
| [self.view.rightAnchor constraintEqualToAnchor:self.navigationBar.rightAnchor].active = YES; |
| [self.navigationBar.topAnchor constraintEqualToAnchor:self.content.bottomAnchor].active = YES; |
| [self.navigationBar.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor].active = YES; |
| } |
| |
| if (@available(iOS 11.0, *)) { |
| [self.navigationBar.barItemsBottomAnchor |
| constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor] |
| .active = YES; |
| } |
| |
| if (@available(iOS 9.0, *)) { |
| // Content View Constraints |
| [self.view.leftAnchor constraintEqualToAnchor:self.content.leftAnchor].active = YES; |
| [self.view.rightAnchor constraintEqualToAnchor:self.content.rightAnchor].active = YES; |
| |
| [self.view.topAnchor constraintEqualToAnchor:self.content.topAnchor].active = YES; |
| } |
| } |
| |
| /** |
| * Pins the given view to the edges of the content view. |
| */ |
| - (void)addConstraintsForContentView:(UIView *)view { |
| view.translatesAutoresizingMaskIntoConstraints = NO; |
| if (@available(iOS 9.0, *)) { |
| [view.leadingAnchor constraintEqualToAnchor:self.content.leadingAnchor].active = YES; |
| [view.trailingAnchor constraintEqualToAnchor:self.content.trailingAnchor].active = YES; |
| [view.topAnchor constraintEqualToAnchor:self.content.topAnchor].active = YES; |
| [view.bottomAnchor constraintEqualToAnchor:self.content.bottomAnchor].active = YES; |
| } else { |
| [NSLayoutConstraint constraintWithItem:self.content |
| attribute:NSLayoutAttributeLeading |
| relatedBy:NSLayoutRelationEqual |
| toItem:view |
| attribute:NSLayoutAttributeLeading |
| multiplier:1 |
| constant:0] |
| .active = YES; |
| [NSLayoutConstraint constraintWithItem:self.content |
| attribute:NSLayoutAttributeTrailing |
| relatedBy:NSLayoutRelationEqual |
| toItem:view |
| attribute:NSLayoutAttributeTrailing |
| multiplier:1 |
| constant:0] |
| .active = YES; |
| [NSLayoutConstraint constraintWithItem:self.content |
| attribute:NSLayoutAttributeTop |
| relatedBy:NSLayoutRelationEqual |
| toItem:view |
| attribute:NSLayoutAttributeTop |
| multiplier:1 |
| constant:0] |
| .active = YES; |
| [NSLayoutConstraint constraintWithItem:self.content |
| attribute:NSLayoutAttributeBottom |
| relatedBy:NSLayoutRelationEqual |
| toItem:view |
| attribute:NSLayoutAttributeBottom |
| multiplier:1 |
| constant:0] |
| .active = YES; |
| } |
| } |
| |
| /** Maps an array of view controllers to their corrisponding tab bar items **/ |
| - (NSArray<UITabBarItem *> *)tabBarItemsForViewControllers: |
| (NSArray<UIViewController *> *)viewControllers { |
| NSMutableArray<UITabBarItem *> *tabBarItems = [NSMutableArray array]; |
| for (UIViewController *viewController in viewControllers) { |
| UITabBarItem *tabBarItem = viewController.tabBarItem; |
| NSAssert(tabBarItem != nil, |
| @"%@'s tabBarItem is nil. Please ensure that each view controller " |
| "added to %@ has set its tab bar item property", |
| viewController, NSStringFromClass([self class])); |
| |
| if (tabBarItem) { |
| [tabBarItems addObject:tabBarItem]; |
| } |
| } |
| |
| return tabBarItems; |
| } |
| |
| /** |
| * Returns an exception for when the navigation bar's items are changed from outside of this class. |
| */ |
| - (NSException *)unauthorizedItemsChangedException { |
| NSString *reason = [NSString |
| stringWithFormat: |
| @"Attempting to set %@'s navigation bar items. Please instead use setViewControllers:", |
| NSStringFromClass([self class])]; |
| return [NSException exceptionWithName:NSInternalInconsistencyException |
| reason:reason |
| userInfo:nil]; |
| } |
| |
| @end |