| // 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 "MDCNavigationBar.h" |
| |
| #import <MDFInternationalization/MDFInternationalization.h> |
| #import <objc/runtime.h> |
| |
| #import <MDFTextAccessibility/MDFTextAccessibility.h> |
| #import "MaterialButtonBar.h" |
| #import "MaterialMath.h" |
| #import "MaterialTypography.h" |
| |
| static const NSUInteger kTitleFontSize = 20; |
| static const CGFloat kNavigationBarDefaultHeight = 56; |
| static const CGFloat kNavigationBarMinHeight = 24; |
| |
| // KVO contexts |
| static char *const kKVOContextMDCNavigationBar = "kKVOContextMDCNavigationBar"; |
| |
| static NSArray<NSString *> *MDCNavigationBarNavigationItemKVOPaths(void) { |
| static dispatch_once_t onceToken; |
| static NSArray<NSString *> *forwardingKeyPaths = nil; |
| dispatch_once(&onceToken, ^{ |
| NSMutableArray<NSString *> *keyPaths = [NSMutableArray array]; |
| |
| Protocol *headerProtocol = @protocol(MDCUINavigationItemObservables); |
| unsigned int count = 0; |
| objc_property_t *propertyList = protocol_copyPropertyList(headerProtocol, &count); |
| if (propertyList) { |
| for (unsigned int ix = 0; ix < count; ++ix) { |
| [keyPaths addObject:[NSString stringWithFormat:@"%s", property_getName(propertyList[ix])]]; |
| } |
| free(propertyList); |
| } |
| |
| // Ensure that the plural bar button item key paths are listened to last, otherwise the |
| // non-plural variant will cause the extra bar button items to be lost. Fun! |
| NSArray<NSString *> *orderedKeyPaths = @[ |
| NSStringFromSelector(@selector(leftBarButtonItems)), |
| NSStringFromSelector(@selector(rightBarButtonItems)) |
| ]; |
| [keyPaths removeObjectsInArray:orderedKeyPaths]; |
| [keyPaths addObjectsFromArray:orderedKeyPaths]; |
| |
| forwardingKeyPaths = keyPaths; |
| }); |
| return forwardingKeyPaths; |
| } |
| |
| @implementation MDCNavigationBarTextColorAccessibilityMutator |
| |
| - (nonnull instancetype)init { |
| self = [super init]; |
| return self; |
| } |
| |
| - (void)mutate:(MDCNavigationBar *)navBar { |
| // Determine what is the appropriate background color |
| UIColor *backgroundColor = navBar.backgroundColor; |
| if (!backgroundColor) { |
| return; |
| } |
| |
| // Update title label color based on navigationBar backgroundColor |
| NSMutableDictionary *textAttr = |
| [NSMutableDictionary dictionaryWithDictionary:[navBar titleTextAttributes]]; |
| UIColor *textColor = |
| [MDFTextAccessibility textColorOnBackgroundColor:backgroundColor |
| targetTextAlpha:1.0 |
| font:[textAttr objectForKey:NSFontAttributeName]]; |
| [textAttr setObject:textColor forKey:NSForegroundColorAttributeName]; |
| [navBar setTitleTextAttributes:textAttr]; |
| |
| // Update button's tint color based on navigationBar backgroundColor |
| navBar.tintColor = textColor; |
| } |
| |
| @end |
| |
| /** |
| Indiana Jones style placeholder view for UINavigationBar. Ownership of UIBarButtonItem.customView |
| and UINavigationItem.titleView are normally transferred to UINavigationController but we plan to |
| steal them away. In order to avoid crashing during KVO updates, we steal the view away and |
| replace it with a sandbag view. |
| */ |
| @interface MDCNavigationBarSandbagView : UIView |
| @end |
| |
| @implementation MDCNavigationBarSandbagView |
| @end |
| |
| @interface MDCNavigationBar (PrivateAPIs) <MDCButtonBarDelegate> |
| |
| /// titleLabel is hidden if there is a titleView. When not hidden, displays self.title. |
| - (UILabel *)titleLabel; |
| - (MDCButtonBar *)leadingButtonBar; |
| - (MDCButtonBar *)trailingButtonBar; |
| |
| @end |
| |
| @implementation MDCNavigationBar { |
| id _observedNavigationItemLock; |
| UINavigationItem *_observedNavigationItem; |
| UIColor *_inkColor; |
| UILabel *_titleLabel; |
| |
| MDCButtonBar *_leadingButtonBar; |
| MDCButtonBar *_trailingButtonBar; |
| |
| __weak UIViewController *_watchingViewController; |
| } |
| |
| @synthesize leadingBarButtonItems = _leadingBarButtonItems; |
| @synthesize trailingBarButtonItems = _trailingBarButtonItems; |
| @synthesize hidesBackButton = _hidesBackButton; |
| @synthesize leadingItemsSupplementBackButton = _leadingItemsSupplementBackButton; |
| @synthesize titleView = _titleView; |
| @synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock; |
| @synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation; |
| |
| - (void)dealloc { |
| [self setObservedNavigationItem:nil]; |
| } |
| |
| - (void)commonMDCNavigationBarInit { |
| _titleInsets = UIEdgeInsetsMake(0, 16, 0, 16); |
| _uppercasesButtonTitles = YES; |
| _observedNavigationItemLock = [[NSObject alloc] init]; |
| _titleFont = [MDCTypography titleFont]; |
| |
| _titleViewLayoutBehavior = MDCNavigationBarTitleViewLayoutBehaviorFill; |
| _titleLabel = [[UILabel alloc] init]; |
| _titleLabel.font = _titleFont; |
| _titleLabel.accessibilityTraits |= UIAccessibilityTraitHeader; |
| _titleLabel.textAlignment = NSTextAlignmentCenter; |
| _leadingButtonBar = [[MDCButtonBar alloc] init]; |
| _leadingButtonBar.layoutPosition = MDCButtonBarLayoutPositionLeading; |
| _leadingButtonBar.delegate = self; |
| _trailingButtonBar = [[MDCButtonBar alloc] init]; |
| _trailingButtonBar.layoutPosition = MDCButtonBarLayoutPositionTrailing; |
| _trailingButtonBar.delegate = self; |
| |
| [self addSubview:_titleLabel]; |
| [self addSubview:_leadingButtonBar]; |
| [self addSubview:_trailingButtonBar]; |
| |
| _mdc_overrideBaseElevation = -1; |
| } |
| |
| - (instancetype)initWithCoder:(NSCoder *)aDecoder { |
| self = [super initWithCoder:aDecoder]; |
| if (self) { |
| [self commonMDCNavigationBarInit]; |
| } |
| return self; |
| } |
| |
| - (instancetype)initWithFrame:(CGRect)frame { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| [self commonMDCNavigationBarInit]; |
| _leadingButtonBar.backgroundColor = nil; |
| _trailingButtonBar.backgroundColor = nil; |
| } |
| return self; |
| } |
| |
| - (void)setUppercasesButtonTitles:(BOOL)uppercasesButtonTitles { |
| _uppercasesButtonTitles = uppercasesButtonTitles; |
| |
| _leadingButtonBar.uppercasesButtonTitles = uppercasesButtonTitles; |
| _trailingButtonBar.uppercasesButtonTitles = uppercasesButtonTitles; |
| } |
| |
| - (void)setTitleFont:(UIFont *)titleFont { |
| if (self.allowAnyTitleFontSize) { |
| _titleFont = titleFont; |
| } else { |
| _titleFont = [UIFont fontWithDescriptor:titleFont.fontDescriptor size:kTitleFontSize]; |
| } |
| if (!_titleFont) { |
| _titleFont = [MDCTypography titleFont]; |
| } |
| _titleLabel.font = _titleFont; |
| } |
| |
| - (void)setTitleTextColor:(UIColor *)titleTextColor { |
| _titleLabel.textColor = titleTextColor; |
| } |
| |
| - (UIColor *)titleTextColor { |
| return _titleLabel.textColor; |
| } |
| |
| #pragma mark Accessibility |
| |
| - (NSArray<__kindof UIView *> *)accessibilityElements { |
| return @[ _leadingButtonBar, self.titleView ? self.titleView : _titleLabel, _trailingButtonBar ]; |
| } |
| |
| - (BOOL)isAccessibilityElement { |
| return NO; |
| } |
| |
| - (NSInteger)accessibilityElementCount { |
| return self.accessibilityElements.count; |
| } |
| |
| - (id)accessibilityElementAtIndex:(NSInteger)index { |
| return self.accessibilityElements[index]; |
| } |
| |
| - (NSInteger)indexOfAccessibilityElement:(id)element { |
| return [self.accessibilityElements indexOfObject:element]; |
| } |
| |
| #pragma mark - MDCButtonBarDelegate |
| |
| - (void)buttonBarDidInvalidateIntrinsicContentSize:(MDCButtonBar *)buttonBar { |
| [self setNeedsLayout]; |
| } |
| |
| #pragma mark UIView Overrides |
| |
| - (void)layoutSubviews { |
| [super layoutSubviews]; |
| |
| // For pre iOS 11 devices, it's safe to assume that the Safe Area insets' left and right |
| // values are zero. DO NOT use this to get the top or bottom Safe Area insets. |
| UIEdgeInsets RTLFriendlySafeAreaInsets = UIEdgeInsetsZero; |
| if (@available(iOS 11.0, *)) { |
| RTLFriendlySafeAreaInsets = MDFInsetsMakeWithLayoutDirection( |
| self.safeAreaInsets.top, self.safeAreaInsets.left, self.safeAreaInsets.bottom, |
| self.safeAreaInsets.right, self.mdf_effectiveUserInterfaceLayoutDirection); |
| } |
| |
| CGSize leadingButtonBarSize = [_leadingButtonBar sizeThatFits:self.bounds.size]; |
| CGRect leadingButtonBarFrame = |
| CGRectMake(RTLFriendlySafeAreaInsets.left, CGRectGetMinY(self.bounds), |
| leadingButtonBarSize.width, leadingButtonBarSize.height); |
| CGSize trailingButtonBarSize = [_trailingButtonBar sizeThatFits:self.bounds.size]; |
| CGFloat xOrigin = |
| CGRectGetWidth(self.bounds) - RTLFriendlySafeAreaInsets.right - trailingButtonBarSize.width; |
| CGRect trailingButtonBarFrame = |
| CGRectMake(xOrigin, CGRectGetMinY(self.bounds), trailingButtonBarSize.width, |
| trailingButtonBarSize.height); |
| if (self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) { |
| leadingButtonBarFrame = |
| MDFRectFlippedHorizontally(leadingButtonBarFrame, CGRectGetWidth(self.bounds)); |
| trailingButtonBarFrame = |
| MDFRectFlippedHorizontally(trailingButtonBarFrame, CGRectGetWidth(self.bounds)); |
| } |
| _leadingButtonBar.frame = leadingButtonBarFrame; |
| _trailingButtonBar.frame = trailingButtonBarFrame; |
| |
| // textFrame is used to determine layout of both TitleLabel and TitleView |
| CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, self.titleInsets); |
| textFrame.origin.x += _leadingButtonBar.frame.size.width; |
| textFrame.size.width -= _leadingButtonBar.frame.size.width + _trailingButtonBar.frame.size.width; |
| if (@available(iOS 11.0, *)) { |
| textFrame.origin.x += self.safeAreaInsets.left; |
| textFrame.size.width -= self.safeAreaInsets.left + self.safeAreaInsets.right; |
| } |
| |
| // Layout TitleLabel |
| NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init]; |
| paraStyle.lineBreakMode = _titleLabel.lineBreakMode; |
| |
| NSDictionary<NSString *, id> *attributes = |
| @{NSFontAttributeName : _titleLabel.font, NSParagraphStyleAttributeName : paraStyle}; |
| |
| CGSize titleSize = [_titleLabel.text boundingRectWithSize:textFrame.size |
| options:NSStringDrawingTruncatesLastVisibleLine |
| attributes:attributes |
| context:NULL] |
| .size; |
| titleSize.width = MDCCeil(titleSize.width); |
| titleSize.height = MDCCeil(titleSize.height); |
| CGRect titleFrame = CGRectMake(textFrame.origin.x, 0, titleSize.width, titleSize.height); |
| if (self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) { |
| titleFrame = MDFRectFlippedHorizontally(titleFrame, CGRectGetWidth(self.bounds)); |
| } |
| UIControlContentVerticalAlignment titleVerticalAlignment = UIControlContentVerticalAlignmentTop; |
| CGRect alignedFrame = [self mdc_frameAlignedVertically:titleFrame |
| withinBounds:textFrame |
| alignment:titleVerticalAlignment]; |
| alignedFrame = [self mdc_frameAlignedHorizontally:alignedFrame alignment:self.titleAlignment]; |
| _titleLabel.frame = MDCRectAlignToScale(alignedFrame, self.window.screen.scale); |
| |
| // Layout TitleView |
| if (self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) { |
| textFrame = MDFRectFlippedHorizontally(textFrame, CGRectGetWidth(self.bounds)); |
| } |
| |
| CGRect titleViewFrame = textFrame; |
| switch (self.titleViewLayoutBehavior) { |
| case MDCNavigationBarTitleViewLayoutBehaviorFill: |
| // Do nothing. The default textFrame calculation will fill the available space. |
| break; |
| |
| case MDCNavigationBarTitleViewLayoutBehaviorCenter: { |
| CGFloat availableWidth = UIEdgeInsetsInsetRect(self.bounds, self.titleInsets).size.width; |
| availableWidth -= |
| MAX(_leadingButtonBar.frame.size.width, _trailingButtonBar.frame.size.width) * 2; |
| if (@available(iOS 11.0, *)) { |
| availableWidth -= self.safeAreaInsets.left + self.safeAreaInsets.right; |
| } |
| titleViewFrame.size.width = availableWidth; |
| titleViewFrame = [self mdc_frameAlignedHorizontally:titleViewFrame |
| alignment:MDCNavigationBarTitleAlignmentCenter]; |
| break; |
| } |
| } |
| // No insets for the titleView, and a height that is the same as the button bars. Clients |
| // can vertically center their titleView subviews to align them with buttons. |
| titleViewFrame.origin.y = 0; |
| CGFloat maxHeight = kNavigationBarDefaultHeight; |
| CGFloat minHeight = kNavigationBarMinHeight; |
| titleViewFrame.size.height = MIN(MAX(self.bounds.size.height, minHeight), maxHeight); |
| self.titleView.frame = titleViewFrame; |
| |
| // Button and title label alignment |
| |
| CGFloat titleTextRectHeight = |
| [_titleLabel textRectForBounds:_titleLabel.bounds limitedToNumberOfLines:0].size.height; |
| |
| if (_titleLabel.hidden || titleTextRectHeight <= 0) { |
| _leadingButtonBar.buttonTitleBaseline = 0; |
| _trailingButtonBar.buttonTitleBaseline = 0; |
| } else { |
| // Assumes that the title is center-aligned vertically. |
| CGFloat titleTextOriginY = (_titleLabel.frame.size.height - titleTextRectHeight) / 2; |
| CGFloat titleTextHeight = titleTextOriginY + titleTextRectHeight + _titleLabel.font.descender; |
| CGFloat titleBaseline = _titleLabel.frame.origin.y + titleTextHeight; |
| _leadingButtonBar.buttonTitleBaseline = titleBaseline; |
| _trailingButtonBar.buttonTitleBaseline = titleBaseline; |
| } |
| } |
| |
| - (CGSize)sizeThatFits:(CGSize)size { |
| CGFloat maxHeight = kNavigationBarDefaultHeight; |
| CGFloat height = MIN(MAX(size.height, kNavigationBarMinHeight), maxHeight); |
| return CGSizeMake(size.width, height); |
| } |
| |
| - (CGSize)intrinsicContentSize { |
| CGFloat height = kNavigationBarDefaultHeight; |
| return CGSizeMake(UIViewNoIntrinsicMetric, height); |
| } |
| |
| - (MDCNavigationBarTitleAlignment)titleAlignment { |
| return [MDCNavigationBar titleAlignmentFromTextAlignment:_titleLabel.textAlignment]; |
| } |
| |
| - (void)setTitleAlignment:(MDCNavigationBarTitleAlignment)titleAlignment { |
| _titleLabel.textAlignment = [MDCNavigationBar textAlignmentFromTitleAlignment:titleAlignment]; |
| [self setNeedsLayout]; |
| } |
| |
| #pragma mark Private |
| |
| + (NSTextAlignment)textAlignmentFromTitleAlignment:(MDCNavigationBarTitleAlignment)titleAlignment { |
| switch (titleAlignment) { |
| case MDCNavigationBarTitleAlignmentCenter: |
| return NSTextAlignmentCenter; |
| break; |
| default: |
| NSAssert(NO, @"titleAlignment not understood: %li", (long)titleAlignment); |
| // Intentional fall through logic |
| case MDCNavigationBarTitleAlignmentLeading: |
| return NSTextAlignmentNatural; |
| break; |
| } |
| } |
| |
| + (MDCNavigationBarTitleAlignment)titleAlignmentFromTextAlignment:(NSTextAlignment)textAlignment { |
| switch (textAlignment) { |
| default: |
| NSAssert(NO, @"textAlignment not supported: %li", (long)textAlignment); |
| // Intentional fall through logic |
| case NSTextAlignmentNatural: |
| case NSTextAlignmentLeft: |
| return MDCNavigationBarTitleAlignmentLeading; |
| break; |
| case NSTextAlignmentCenter: |
| return MDCNavigationBarTitleAlignmentCenter; |
| break; |
| } |
| } |
| |
| - (UILabel *)titleLabel { |
| return _titleLabel; |
| } |
| |
| - (MDCButtonBar *)leadingButtonBar { |
| return _leadingButtonBar; |
| } |
| |
| - (MDCButtonBar *)trailingButtonBar { |
| return _trailingButtonBar; |
| } |
| |
| #pragma mark KVO |
| |
| - (void)observeValueForKeyPath:(NSString *)keyPath |
| ofObject:(id)object |
| change:(NSDictionary *)change |
| context:(void *)context { |
| if (context == kKVOContextMDCNavigationBar) { |
| void (^mainThreadWork)(void) = ^{ |
| @synchronized(self->_observedNavigationItemLock) { |
| if (object != self->_observedNavigationItem) { |
| return; |
| } |
| |
| [self setValue:[object valueForKey:keyPath] forKey:keyPath]; |
| } |
| }; |
| |
| // 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 TraitCollection |
| |
| - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { |
| [super traitCollectionDidChange:previousTraitCollection]; |
| |
| if (self.traitCollectionDidChangeBlock) { |
| self.traitCollectionDidChangeBlock(self, previousTraitCollection); |
| } |
| } |
| |
| #pragma mark Layout |
| |
| - (CGRect)mdc_frameAlignedVertically:(CGRect)frame |
| withinBounds:(CGRect)bounds |
| alignment:(UIControlContentVerticalAlignment)alignment { |
| switch (alignment) { |
| case UIControlContentVerticalAlignmentBottom: |
| return CGRectMake(CGRectGetMinX(frame), CGRectGetMaxY(bounds) - CGRectGetHeight(frame), |
| CGRectGetWidth(frame), CGRectGetHeight(frame)); |
| |
| case UIControlContentVerticalAlignmentCenter: { |
| CGFloat centeredY = |
| MDCFloor((CGRectGetHeight(bounds) - CGRectGetHeight(frame)) / 2) + CGRectGetMinY(bounds); |
| return CGRectMake(CGRectGetMinX(frame), centeredY, CGRectGetWidth(frame), |
| CGRectGetHeight(frame)); |
| } |
| |
| case UIControlContentVerticalAlignmentTop: { |
| // The title frame is vertically centered with the back button but will stick to the top of |
| // the header regardless of the header's height. |
| CGFloat maxHeight = kNavigationBarDefaultHeight; |
| CGFloat height = MIN(CGRectGetHeight(bounds), maxHeight); |
| CGFloat navigationBarCenteredY = MDCFloor((height - CGRectGetHeight(frame)) / 2); |
| navigationBarCenteredY = MAX(0, navigationBarCenteredY); |
| return CGRectMake(CGRectGetMinX(frame), navigationBarCenteredY, CGRectGetWidth(frame), |
| CGRectGetHeight(frame)); |
| } |
| |
| case UIControlContentVerticalAlignmentFill: { |
| return bounds; |
| } |
| } |
| } |
| |
| - (CGRect)mdc_frameAlignedHorizontally:(CGRect)frame |
| alignment:(MDCNavigationBarTitleAlignment)alignment { |
| switch (alignment) { |
| // Center align title |
| case MDCNavigationBarTitleAlignmentCenter: { |
| BOOL isRTL = [self mdf_effectiveUserInterfaceLayoutDirection] == |
| UIUserInterfaceLayoutDirectionRightToLeft; |
| |
| MDCButtonBar *leftButtonBar = self.leadingButtonBar; |
| MDCButtonBar *rightButtonBar = self.trailingButtonBar; |
| CGFloat titleLeftInset = self.titleInsets.left; |
| CGFloat titleRightInset = self.titleInsets.right; |
| |
| if (isRTL) { |
| leftButtonBar = self.trailingButtonBar; |
| rightButtonBar = self.leadingButtonBar; |
| titleLeftInset = self.titleInsets.right; |
| titleRightInset = self.titleInsets.left; |
| } |
| |
| // Determine how much space is available to the left/right of the navigation bar's midpoint |
| CGFloat midX = CGRectGetMidX(self.bounds); |
| CGFloat leftMidSpaceX = midX - CGRectGetMaxX(leftButtonBar.frame) - titleLeftInset; |
| CGFloat rightMidSpaceX = CGRectGetMinX(rightButtonBar.frame) - midX - titleRightInset; |
| CGFloat halfFrameWidth = CGRectGetWidth(frame) / 2; |
| |
| // Place the title in the exact center if we have enough left/right space |
| if (leftMidSpaceX >= halfFrameWidth && rightMidSpaceX >= halfFrameWidth) { |
| CGFloat xOrigin = CGRectGetMaxX(self.bounds) / 2 - CGRectGetWidth(frame) / 2; |
| return CGRectMake(xOrigin, CGRectGetMinY(frame), CGRectGetWidth(frame), |
| CGRectGetHeight(frame)); |
| } |
| |
| // Place the title as close to the center, shifting it slightly in to the side with more space |
| if (leftMidSpaceX >= halfFrameWidth) { |
| CGFloat frameMaxX = CGRectGetMinX(rightButtonBar.frame) - titleRightInset; |
| return CGRectMake(frameMaxX - CGRectGetWidth(frame), CGRectGetMinY(frame), |
| CGRectGetWidth(frame), CGRectGetHeight(frame)); |
| } |
| if (rightMidSpaceX >= halfFrameWidth) { |
| CGFloat frameOriginX = CGRectGetMaxX(leftButtonBar.frame) + titleLeftInset; |
| return CGRectMake(frameOriginX, CGRectGetMinY(frame), CGRectGetWidth(frame), |
| CGRectGetHeight(frame)); |
| } |
| } |
| // Intentional fall through |
| case MDCNavigationBarTitleAlignmentLeading: |
| return frame; |
| } |
| } |
| |
| - (NSArray<UIBarButtonItem *> *)mdc_buttonItemsForLeadingBar { |
| if (!self.leadingItemsSupplementBackButton && self.leadingBarButtonItems.count > 0) { |
| return self.leadingBarButtonItems; |
| } |
| |
| NSMutableArray<UIBarButtonItem *> *buttonItems = [NSMutableArray array]; |
| if (self.backItem && !self.hidesBackButton) { |
| [buttonItems addObject:self.backItem]; |
| } |
| [buttonItems addObjectsFromArray:self.leadingBarButtonItems]; |
| return buttonItems; |
| } |
| |
| #pragma mark Colors |
| |
| - (void)tintColorDidChange { |
| [super tintColorDidChange]; |
| |
| // Tint color should only modify interactive elements |
| _leadingButtonBar.tintColor = self.leadingBarItemsTintColor ?: self.tintColor; |
| _trailingButtonBar.tintColor = self.trailingBarItemsTintColor ?: self.tintColor; |
| } |
| |
| #pragma mark Public |
| |
| - (void)setTitle:(NSString *)title { |
| // |self.titleTextAttributes| can only be set if |title| is set |
| if (self.titleTextAttributes && title.length > 0) { |
| _titleLabel.attributedText = [[NSAttributedString alloc] initWithString:title |
| attributes:_titleTextAttributes]; |
| } else { |
| _titleLabel.text = title; |
| } |
| [self setNeedsLayout]; |
| } |
| |
| - (NSString *)title { |
| return _titleLabel.text; |
| } |
| |
| - (void)setTitleView:(UIView *)titleView { |
| // Ignore sandbag KVO events |
| if ([_observedNavigationItem.titleView isKindOfClass:[MDCNavigationBarSandbagView class]]) { |
| return; |
| } |
| |
| // Swap in the sandbag (so that UINavigationController won't steal our view). It's important that |
| // we do this before we add titleView as a subview, because starting from iOS 11 the navigation |
| // item will call |removeFromSuperview| on the old titleView when we replace it. |
| if (titleView) { |
| _observedNavigationItem.titleView = [[MDCNavigationBarSandbagView alloc] init]; |
| } else if (_observedNavigationItem.titleView) { |
| _observedNavigationItem.titleView = nil; |
| } |
| |
| if (self.titleView != titleView) { |
| [self.titleView removeFromSuperview]; |
| _titleView = titleView; |
| } |
| |
| if (_titleView != nil) { |
| [self addSubview:_titleView]; |
| } |
| |
| _titleLabel.hidden = _titleView != nil; |
| |
| [self setNeedsLayout]; |
| } |
| |
| - (void)setTitleTextAttributes:(NSDictionary<NSString *, id> *)titleTextAttributes { |
| // If title dictionary is equivalent, no need to make changes |
| if ([_titleTextAttributes isEqualToDictionary:titleTextAttributes]) { |
| return; |
| } |
| |
| // Copy attributes dictionary |
| _titleTextAttributes = [titleTextAttributes copy]; |
| if (_titleLabel) { |
| // |_titleTextAttributes| can only be set if |self.title| is set |
| if (_titleTextAttributes && self.title.length > 0) { |
| // Set label text as newly created attributed string with attributes if non-nil |
| _titleLabel.attributedText = [[NSAttributedString alloc] initWithString:self.title |
| attributes:_titleTextAttributes]; |
| } else { |
| // Otherwise set titleLabel text property |
| _titleLabel.text = self.title; |
| } |
| [self setNeedsLayout]; |
| } |
| } |
| |
| - (void)setButtonsTitleFont:(UIFont *)font forState:(UIControlState)state { |
| [_leadingButtonBar setButtonsTitleFont:font forState:state]; |
| [_trailingButtonBar setButtonsTitleFont:font forState:state]; |
| } |
| |
| - (UIFont *)buttonsTitleFontForState:(UIControlState)state { |
| return [_leadingButtonBar buttonsTitleFontForState:state]; |
| } |
| |
| - (void)setButtonsTitleColor:(UIColor *)color forState:(UIControlState)state { |
| [_leadingButtonBar setButtonsTitleColor:color forState:state]; |
| [_trailingButtonBar setButtonsTitleColor:color forState:state]; |
| } |
| |
| - (UIColor *)buttonsTitleColorForState:(UIControlState)state { |
| return [_leadingButtonBar buttonsTitleColorForState:state]; |
| } |
| |
| - (void)setLeadingBarItemsTintColor:(UIColor *)leadingBarItemsTintColor { |
| _leadingBarItemsTintColor = leadingBarItemsTintColor; |
| self.leadingButtonBar.tintColor = leadingBarItemsTintColor; |
| } |
| |
| - (void)setTrailingBarItemsTintColor:(UIColor *)trailingBarItemsTintColor { |
| _trailingBarItemsTintColor = trailingBarItemsTintColor; |
| self.trailingButtonBar.tintColor = trailingBarItemsTintColor; |
| } |
| |
| - (void)setLeadingBarButtonItems:(NSArray<UIBarButtonItem *> *)leadingBarButtonItems { |
| _leadingBarButtonItems = [leadingBarButtonItems copy]; |
| _leadingButtonBar.items = [self mdc_buttonItemsForLeadingBar]; |
| [self setNeedsLayout]; |
| } |
| |
| - (void)setTrailingBarButtonItems:(NSArray<UIBarButtonItem *> *)trailingBarButtonItems { |
| _trailingBarButtonItems = [trailingBarButtonItems copy]; |
| _trailingButtonBar.items = _trailingBarButtonItems; |
| [self setNeedsLayout]; |
| } |
| |
| - (void)setLeadingBarButtonItem:(UIBarButtonItem *)leadingBarButtonItem { |
| self.leadingBarButtonItems = leadingBarButtonItem ? @[ leadingBarButtonItem ] : nil; |
| } |
| |
| - (UIBarButtonItem *)leadingBarButtonItem { |
| return [self.leadingBarButtonItems firstObject]; |
| } |
| |
| - (void)setTrailingBarButtonItem:(UIBarButtonItem *)trailingBarButtonItem { |
| self.trailingBarButtonItems = trailingBarButtonItem ? @[ trailingBarButtonItem ] : nil; |
| } |
| |
| - (UIBarButtonItem *)trailingBarButtonItem { |
| return [self.trailingBarButtonItems firstObject]; |
| } |
| |
| - (CGRect)rectForLeadingBarButtonItem:(nonnull UIBarButtonItem *)item |
| inCoordinateSpace:(nonnull id<UICoordinateSpace>)coordinateSpace { |
| return [self.leadingButtonBar rectForItem:item inCoordinateSpace:coordinateSpace]; |
| } |
| |
| - (CGRect)rectForTrailingBarButtonItem:(nonnull UIBarButtonItem *)item |
| inCoordinateSpace:(nonnull id<UICoordinateSpace>)coordinateSpace { |
| return [self.trailingButtonBar rectForItem:item inCoordinateSpace:coordinateSpace]; |
| } |
| |
| - (void)setBackBarButtonItem:(UIBarButtonItem *)backBarButtonItem { |
| self.backItem = backBarButtonItem; |
| } |
| |
| - (UIBarButtonItem *)backBarButtonItem { |
| return self.backItem; |
| } |
| |
| - (void)setBackItem:(UIBarButtonItem *)backItem { |
| if (_backItem == backItem) { |
| return; |
| } |
| _backItem = backItem; |
| _leadingButtonBar.items = [self mdc_buttonItemsForLeadingBar]; |
| [self setNeedsLayout]; |
| } |
| |
| - (void)setHidesBackButton:(BOOL)hidesBackButton { |
| if (_hidesBackButton == hidesBackButton) { |
| return; |
| } |
| _hidesBackButton = hidesBackButton; |
| _leadingButtonBar.items = [self mdc_buttonItemsForLeadingBar]; |
| [self setNeedsLayout]; |
| } |
| |
| - (void)setLeadingItemsSupplementBackButton:(BOOL)leadingItemsSupplementBackButton { |
| if (_leadingItemsSupplementBackButton == leadingItemsSupplementBackButton) { |
| return; |
| } |
| _leadingItemsSupplementBackButton = leadingItemsSupplementBackButton; |
| _leadingButtonBar.items = [self mdc_buttonItemsForLeadingBar]; |
| [self setNeedsLayout]; |
| } |
| |
| - (UIColor *)inkColor { |
| return _inkColor; |
| } |
| |
| - (void)setInkColor:(UIColor *)inkColor { |
| if (_inkColor == inkColor) { |
| return; |
| } |
| _inkColor = inkColor; |
| _leadingButtonBar.inkColor = inkColor; |
| _trailingButtonBar.inkColor = inkColor; |
| } |
| |
| - (void)setRippleColor:(UIColor *)rippleColor { |
| if (_rippleColor == rippleColor || [_rippleColor isEqual:rippleColor]) { |
| return; |
| } |
| _rippleColor = rippleColor; |
| |
| _leadingButtonBar.rippleColor = rippleColor; |
| _trailingButtonBar.rippleColor = rippleColor; |
| } |
| |
| - (void)setEnableRippleBehavior:(BOOL)enableRippleBehavior { |
| if (_enableRippleBehavior == enableRippleBehavior) { |
| return; |
| } |
| _enableRippleBehavior = enableRippleBehavior; |
| |
| _leadingButtonBar.enableRippleBehavior = enableRippleBehavior; |
| _trailingButtonBar.enableRippleBehavior = enableRippleBehavior; |
| } |
| |
| - (CGFloat)mdc_currentElevation { |
| return 0; |
| } |
| |
| - (void)setObservedNavigationItem:(UINavigationItem *)navigationItem { |
| @synchronized(_observedNavigationItemLock) { |
| if (navigationItem == _observedNavigationItem) { |
| return; |
| } |
| |
| NSArray<NSString *> *keyPaths = MDCNavigationBarNavigationItemKVOPaths(); |
| for (NSString *keyPath in keyPaths) { |
| [_observedNavigationItem removeObserver:self |
| forKeyPath:keyPath |
| context:kKVOContextMDCNavigationBar]; |
| } |
| |
| _observedNavigationItem = navigationItem; |
| |
| NSKeyValueObservingOptions options = |
| (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial); |
| for (NSString *keyPath in keyPaths) { |
| [_observedNavigationItem addObserver:self |
| forKeyPath:keyPath |
| options:options |
| context:kKVOContextMDCNavigationBar]; |
| } |
| } |
| } |
| |
| - (void)observeNavigationItem:(UINavigationItem *)navigationItem { |
| [self setObservedNavigationItem:navigationItem]; |
| } |
| |
| - (void)unobserveNavigationItem { |
| [self setObservedNavigationItem:nil]; |
| } |
| |
| #pragma mark UINavigationItem interface matching |
| |
| - (NSArray<UIBarButtonItem *> *)leftBarButtonItems { |
| return self.leadingBarButtonItems; |
| } |
| |
| - (void)setLeftBarButtonItems:(NSArray<UIBarButtonItem *> *)leftBarButtonItems { |
| self.leadingBarButtonItems = leftBarButtonItems; |
| } |
| |
| - (NSArray<UIBarButtonItem *> *)rightBarButtonItems { |
| return self.trailingBarButtonItems; |
| } |
| |
| - (void)setRightBarButtonItems:(NSArray<UIBarButtonItem *> *)rightBarButtonItems { |
| self.trailingBarButtonItems = rightBarButtonItems; |
| } |
| |
| - (UIBarButtonItem *)leftBarButtonItem { |
| return self.leadingBarButtonItem; |
| } |
| |
| - (void)setLeftBarButtonItem:(UIBarButtonItem *)leftBarButtonItem { |
| self.leadingBarButtonItem = leftBarButtonItem; |
| } |
| |
| - (UIBarButtonItem *)rightBarButtonItem { |
| return self.trailingBarButtonItem; |
| } |
| |
| - (void)setRightBarButtonItem:(UIBarButtonItem *)rightBarButtonItem { |
| self.trailingBarButtonItem = rightBarButtonItem; |
| } |
| |
| - (BOOL)leftItemsSupplementBackButton { |
| return self.leadingItemsSupplementBackButton; |
| } |
| |
| - (void)setLeftItemsSupplementBackButton:(BOOL)leftItemsSupplementBackButton { |
| self.leadingItemsSupplementBackButton = leftItemsSupplementBackButton; |
| } |
| |
| @end |