From b1bd6f2d1febdb51164fd4110155d1f62f9f792f Mon Sep 17 00:00:00 2001 From: Frank Wang Date: Mon, 18 Nov 2024 12:06:18 -0800 Subject: [PATCH] [NavigationSuite] Introduce custom class MDCBottomNavigationBarItem that supports setting a custom badge appearance for each UITabBarItem. Add API to MDCBottomNavigationBar to allow users to set an array of MDCBottomNavigationBarItems. PiperOrigin-RevId: 697712760 --- .../src/MDCBottomNavigationBar.h | 16 ++ .../src/MDCBottomNavigationBar.m | 258 +++++++++++++++++- .../src/MDCBottomNavigationBarItem.h | 50 ++++ .../src/MDCBottomNavigationBarItem.m | 36 +++ .../src/private/MDCBottomNavigationItemView.m | 16 ++ 5 files changed, 364 insertions(+), 12 deletions(-) create mode 100644 components/BottomNavigation/src/MDCBottomNavigationBarItem.h create mode 100644 components/BottomNavigation/src/MDCBottomNavigationBarItem.m diff --git a/components/BottomNavigation/src/MDCBottomNavigationBar.h b/components/BottomNavigation/src/MDCBottomNavigationBar.h index f2987a7eed7..3d5f51e7736 100644 --- a/components/BottomNavigation/src/MDCBottomNavigationBar.h +++ b/components/BottomNavigation/src/MDCBottomNavigationBar.h @@ -19,6 +19,7 @@ // TODO(b/151929968): Delete import of MDCBottomNavigationBarDelegate.h when client code has been // migrated to no longer import MDCBottomNavigationBarDelegate as a transitive dependency. #import "MDCBottomNavigationBarDelegate.h" +#import "MDCBottomNavigationBarItem.h" #import "MaterialElevation.h" #import "MDCMinimumOS.h" // IWYU pragma: keep #import "MaterialShadow.h" @@ -117,14 +118,29 @@ typedef NS_ENUM(NSInteger, MDCBottomNavigationBarAlignment) { recommended the array contain at least three items and no more than five items -- appearance may degrade outside of this range. */ +// TODO(b/378528228): Remove this property once all clients have migrated to using `barItems`. @property(nonatomic, copy, nonnull) NSArray *items; +/** + An array of MDCBottomNavigationBarItems that is used to populate bottom navigation bar content. It + is strongly recommended the array contain at least three items and no more than five items -- + appearance may degrade outside of this range. + */ +@property(nonatomic, copy, nonnull) NSArray *barItems; + /** Selected item in the bottom navigation bar. Default is no item selected. */ +// TODO(b/378528228): Remove this property once all clients have migrated to using `barItems`. @property(nonatomic, weak, nullable) UITabBarItem *selectedItem; +/** + Selected MDCBottomNavigationBarItem in the bottom navigation bar. + Default is no item selected. + */ +@property(nonatomic, weak, nullable) MDCBottomNavigationBarItem *selectedBarItem; + /** Display font used for item titles. Default is system font. diff --git a/components/BottomNavigation/src/MDCBottomNavigationBar.m b/components/BottomNavigation/src/MDCBottomNavigationBar.m index 851b000430f..0ab4c471a90 100644 --- a/components/BottomNavigation/src/MDCBottomNavigationBar.m +++ b/components/BottomNavigation/src/MDCBottomNavigationBar.m @@ -13,6 +13,7 @@ // limitations under the License. #import #import +#import #import "MDCAvailability.h" #import "MDCBottomNavigationBar.h" @@ -22,6 +23,7 @@ #import "private/MDCBottomNavigationItemView.h" #import "MDCBadgeAppearance.h" #import "MDCBottomNavigationBarDelegate.h" +#import "MDCBottomNavigationBarItem.h" #import "MDCBottomNavigationBar+ItemView.h" #import "MDCPalettes.h" #import "MDCRippleTouchController.h" @@ -39,6 +41,7 @@ // KVO context static char *const kKVOContextMDCBottomNavigationBar = "kKVOContextMDCBottomNavigationBar"; +static char *const kKVOContextMDCBottomNavigationBarItem = "kKVOContextMDCBottomNavigationBarItem"; static const CGFloat kMinItemWidth = 80; static const CGFloat kPreferredItemWidth = 120; @@ -246,6 +249,10 @@ - (void)safeAreaInsetsDidChange { [self setNeedsLayout]; } +- (NSUInteger)itemCount { + return self.barItems.count > 0 ? self.barItems.count : self.items.count; +} + - (CGSize)intrinsicContentSize { if (self.enableVerticalLayout) { return CGSizeMake([self barWidthForVerticalLayout], UIViewNoIntrinsicMetric); @@ -253,7 +260,8 @@ - (CGSize)intrinsicContentSize { CGFloat height = [self calculateBarHeight]; CGFloat itemWidth = [self widthForItemsWhenCenteredWithAvailableWidth:CGFLOAT_MAX height:height]; - return CGSizeMake(itemWidth * self.items.count, height); + + return CGSizeMake(itemWidth * [self itemCount], height); } } @@ -266,9 +274,10 @@ - (CGFloat)widthForItemsWhenCenteredWithAvailableWidth:(CGFloat)availableWidth self.itemsHorizontalPadding * 2); } maxItemWidth = MIN(kMaxItemWidth, maxItemWidth); - CGFloat totalWidth = maxItemWidth * self.items.count; + NSUInteger itemCount = [self itemCount]; + CGFloat totalWidth = maxItemWidth * itemCount; if (totalWidth > availableWidth) { - maxItemWidth = availableWidth / self.items.count; + maxItemWidth = availableWidth / itemCount; } if (maxItemWidth < kMinItemWidth) { maxItemWidth = kMinItemWidth; @@ -475,6 +484,18 @@ - (void)activateHorizontalLayoutConstraints { - (void)dealloc { [self removeObserversFromTabBarItems]; + [self removeObserversFromBarItems]; +} + +- (NSArray *)barItemKVOKeyPaths { + static NSArray *keyPaths; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keyPaths = @[ + NSStringFromSelector(@selector(item)), + ]; + }); + return keyPaths; } - (NSArray *)kvoKeyPaths { @@ -501,6 +522,56 @@ - (void)dealloc { return keyPaths; } +- (void)addObserversToTabBarItemsForBarItems { + NSArray *keyPaths = [self kvoKeyPaths]; + NSArray *barItemKeyPaths = [self barItemKVOKeyPaths]; + for (MDCBottomNavigationBarItem *barItem in self.barItems) { + for (NSString *keyPath in keyPaths) { + [barItem.item addObserver:self + forKeyPath:keyPath + options:NSKeyValueObservingOptionNew + context:kKVOContextMDCBottomNavigationBar]; + } + for (NSString *keyPath in barItemKeyPaths) { + [barItem addObserver:self + forKeyPath:keyPath + options:NSKeyValueObservingOptionNew + context:kKVOContextMDCBottomNavigationBarItem]; + } + } +} + +- (void)removeObserversFromBarItems { + NSArray *keyPaths = [self kvoKeyPaths]; + NSArray *barItemKeyPaths = [self barItemKVOKeyPaths]; + for (MDCBottomNavigationBarItem *barItem in self.barItems) { + for (NSString *keyPath in keyPaths) { + @try { + [barItem.item removeObserver:self + forKeyPath:keyPath + context:kKVOContextMDCBottomNavigationBar]; + } @catch (NSException *exception) { + if (exception) { + // No need to do anything if there are no observers. + } + } + } + for (NSString *keyPath in barItemKeyPaths) { + @try { + [barItem removeObserver:self + forKeyPath:keyPath + context:kKVOContextMDCBottomNavigationBarItem]; + } @catch (NSException *exception) { + if (exception) { + // No need to do anything if there are no observers. + } + } + } + } +} + +// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to +// setBarItems. - (void)addObserversToTabBarItems { NSArray *keyPaths = [self kvoKeyPaths]; for (UITabBarItem *item in self.items) { @@ -513,6 +584,8 @@ - (void)addObserversToTabBarItems { } } +// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to +// setBarItems. - (void)removeObserversFromTabBarItems { NSArray *keyPaths = [self kvoKeyPaths]; for (UITabBarItem *item in self.items) { @@ -537,6 +610,18 @@ - (void)observeValueForKeyPath:(nullable NSString *)keyPath return; } NSUInteger itemIndex = [self.items indexOfObject:object]; + // Since the object returned is of type UITabBarItem, we need to create an array from the + // barItems array and get the index of the UITabBarItem. + if (self.barItems.count > 0) { + itemIndex = NSNotFound; + for (NSUInteger i = 0; i < self.barItems.count; i++) { + if ([self.barItems[i].item isEqual:object]) { + itemIndex = i; + break; + } + } + } + if (itemIndex == NSNotFound || itemIndex >= _itemViews.count) { return; } @@ -576,6 +661,25 @@ - (void)observeValueForKeyPath:(nullable NSString *)keyPath isEqualToString:NSStringFromSelector(@selector(largeContentSizeImageInsets))]) { itemView.largeContentImageInsets = [newValue UIEdgeInsetsValue]; } + } else if (context == kKVOContextMDCBottomNavigationBarItem) { + if (!object) { + return; + } + NSUInteger itemIndex = [self.barItems indexOfObject:object]; + + if (itemIndex == NSNotFound || itemIndex >= _itemViews.count) { + return; + } + id newValue = [object valueForKey:keyPath]; + if (newValue == [NSNull null]) { + newValue = nil; + } + + if ([keyPath isEqualToString:NSStringFromSelector(@selector(item))]) { + // Remove all existing item views and repopulate them with the new bar items. + [self removeItemViews]; + [self updateBarItems]; + } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } @@ -599,7 +703,7 @@ - (nullable UIView *)viewForItem:(UITabBarItem *)item { } - (nullable UITabBarItem *)tabBarItemForPoint:(CGPoint)point { - for (NSUInteger i = 0; (i < self.itemViews.count) && (i < self.items.count); i++) { + for (NSUInteger i = 0; (i < self.itemViews.count) && (i < [self itemCount]); i++) { UIView *itemView = self.itemViews[i]; BOOL isPointInView = CGRectContainsPoint(itemView.frame, point); if (isPointInView) { @@ -638,6 +742,27 @@ - (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitColl #pragma mark - Touch handlers +- (void)didTouchUpInsidebarItemButton:(UIButton *)button { + for (NSUInteger i = 0; i < self.barItems.count; i++) { + MDCBottomNavigationBarItem *barItem = self.barItems[i]; + MDCBottomNavigationItemView *itemView = self.itemViews[i]; + if (itemView.button == button) { + BOOL shouldSelect = YES; + if ([self.delegate respondsToSelector:@selector(bottomNavigationBar:shouldSelectItem:)]) { + shouldSelect = [self.delegate bottomNavigationBar:self shouldSelectItem:barItem.item]; + } + if (shouldSelect) { + [self setSelectedBarItem:barItem animated:YES]; + if ([self.delegate respondsToSelector:@selector(bottomNavigationBar:didSelectItem:)]) { + [self.delegate bottomNavigationBar:self didSelectItem:barItem.item]; + } + } + } + } +} + +// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to +// setBarItems. - (void)didTouchUpInsideButton:(UIButton *)button { for (NSUInteger i = 0; i < self.items.count; i++) { UITabBarItem *item = self.items[i]; @@ -659,8 +784,110 @@ - (void)didTouchUpInsideButton:(UIButton *)button { #pragma mark - Setters +- (void)setBarItems:(NSArray *)barItems { + if ([_barItems isEqual:barItems] || _barItems == barItems) { + return; + } + // If clients report conflicting gesture recognizers please see proposed solution in the + // internal document: go/mdc-ios-bottomnavigation-largecontentvieweritem + [self addInteraction:[[UILargeContentViewerInteraction alloc] initWithDelegate:self]]; + + [self removeItemViews]; + _barItems = [barItems copy]; + [self updateBarItems]; +} + +- (void)removeItemViews { + // Remove existing item views from the bottom navigation so it can be repopulated with new items. + for (MDCBottomNavigationItemView *itemView in self.itemViews) { + [itemView removeFromSuperview]; + } + if (self.itemViews.count > 0) { + [self.itemViews removeAllObjects]; + [self.itemViewHeightConstraints removeAllObjects]; + [self.itemViewWidthConstraints removeAllObjects]; + [self removeObserversFromBarItems]; + } +} + +- (void)updateBarItems { + CGFloat barHeight = [self calculateBarHeight]; + + for (NSUInteger i = 0; i < self.barItems.count; i++) { + MDCBottomNavigationItemView *itemView = + [[MDCBottomNavigationItemView alloc] initWithFrame:CGRectZero]; + + itemView.rippleTouchController.delegate = self; + itemView.selected = NO; + itemView.displayTitleInVerticalLayout = self.displayItemTitlesInVerticalLayout; + itemView.enableVerticalLayout = self.enableVerticalLayout; + + itemView.selectionIndicatorColor = self.selectionIndicatorColor; + itemView.selectionIndicatorSize = self.selectionIndicatorSize; + [self configureTitleStateForItemView:itemView]; + [self configureItemView:itemView withItem:self.barItems[i].item]; + // TODO(b/378528228): Consolidate this inside configureItemView once clients are fully migrated + // to setBarItems. + itemView.badgeAppearance = self.itemBadgeAppearance; + if (self.barItems[i].badgeAppearance != nil) { + MDCBadgeAppearance *_Nonnull nonnullAppearance = self.barItems[i].badgeAppearance; + itemView.badgeAppearance = nonnullAppearance; + } + + [itemView.button addTarget:self + action:@selector(didTouchUpInsidebarItemButton:) + forControlEvents:UIControlEventTouchUpInside]; + + [self.itemViews addObject:itemView]; + [self.itemsLayoutView addArrangedSubview:itemView]; + itemView.translatesAutoresizingMaskIntoConstraints = NO; + NSLayoutConstraint *itemViewHeightConstraint = + [itemView.heightAnchor constraintEqualToConstant:barHeight]; + // This priority is set to low to avoid conflict of constraints due to the itemView's height + // being the same as the bar. + itemViewHeightConstraint.priority = UILayoutPriorityDefaultLow; + [self.itemViewHeightConstraints addObject:itemViewHeightConstraint]; + NSLayoutConstraint *itemViewWidthConstraint = + [itemView.widthAnchor constraintEqualToConstant:kDefaultVerticalLayoutWidth]; + // This priority is set to low to avoid conflict of constraints due to the itemView's width + // being the same as the bar. + itemViewWidthConstraint.priority = UILayoutPriorityDefaultLow; + [self.itemViewWidthConstraints addObject:itemViewWidthConstraint]; + } + + self.selectedBarItem = nil; + [NSLayoutConstraint activateConstraints:self.itemViewHeightConstraints]; + [self loadConstraints]; + [self addObserversToTabBarItemsForBarItems]; + [self invalidateIntrinsicContentSize]; + [self setNeedsLayout]; +} + +- (void)setSelectedBarItem:(nullable MDCBottomNavigationBarItem *)selectedBarItem { + [self setSelectedBarItem:selectedBarItem animated:NO]; +} + +- (void)setSelectedBarItem:(nullable MDCBottomNavigationBarItem *)selectedBarItem + animated:(BOOL)animated { + if (_selectedBarItem == selectedBarItem) { + return; + } + _selectedBarItem = selectedBarItem; + for (NSUInteger i = 0; i < self.barItems.count; i++) { + MDCBottomNavigationBarItem *barItem = self.barItems[i]; + MDCBottomNavigationItemView *itemView = self.itemViews[i]; + if (selectedBarItem == barItem) { + [itemView setSelected:YES animated:animated]; + } else { + [itemView setSelected:NO animated:animated]; + } + } +} + +// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to +// setBarItems. - (void)setItems:(NSArray *)items { - if ([_items isEqual:items] || _items == items) { + if ([_items isEqual:items] || _items == items || _barItems.count > 0) { return; } // If clients report conflicting gesture recognizers please see proposed solution in the @@ -722,10 +949,14 @@ - (void)setItems:(NSArray *)items { [self setNeedsLayout]; } +// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to +// setBarItems. - (void)setSelectedItem:(nullable UITabBarItem *)selectedItem { [self setSelectedItem:selectedItem animated:NO]; } +// TODO(b/378528228): Remove this function and associated logic when clients are fully migrated to +// setBarItems. - (void)setSelectedItem:(UITabBarItem *)selectedItem animated:(BOOL)animated { if (_selectedItem == selectedItem) { return; @@ -747,7 +978,7 @@ - (void)setItemsContentVerticalMargin:(CGFloat)itemsContentsVerticalMargin { return; } _itemsContentVerticalMargin = itemsContentsVerticalMargin; - for (NSUInteger i = 0; i < self.items.count; i++) { + for (NSUInteger i = 0; i < [self itemCount]; i++) { MDCBottomNavigationItemView *itemView = self.itemViews[i]; itemView.contentVerticalMargin = itemsContentsVerticalMargin; } @@ -760,7 +991,7 @@ - (void)setItemsContentHorizontalMargin:(CGFloat)itemsContentHorizontalMargin { return; } _itemsContentHorizontalMargin = itemsContentHorizontalMargin; - for (NSUInteger i = 0; i < self.items.count; i++) { + for (NSUInteger i = 0; i < [self itemCount]; i++) { MDCBottomNavigationItemView *itemView = self.itemViews[i]; itemView.contentHorizontalMargin = itemsContentHorizontalMargin; } @@ -1045,7 +1276,7 @@ + (BOOL)enablePerformantShadow { - (void)setRippleColor:(nullable UIColor *)rippleColor { _rippleColor = rippleColor; - for (NSUInteger i = 0; i < self.items.count; ++i) { + for (NSUInteger i = 0; i < [self itemCount]; ++i) { MDCBottomNavigationItemView *itemView = self.itemViews[i]; itemView.rippleColor = _rippleColor; } @@ -1056,16 +1287,19 @@ - (void)setRippleColor:(nullable UIColor *)rippleColor { - (void)setItemBadgeAppearance:(MDCBadgeAppearance *)itemBadgeAppearance { _itemBadgeAppearance = [itemBadgeAppearance copy]; - for (NSUInteger i = 0; i < self.items.count; ++i) { + for (NSUInteger i = 0; i < [self itemCount]; ++i) { MDCBottomNavigationItemView *itemView = self.itemViews[i]; - itemView.badgeAppearance = _itemBadgeAppearance; + if (self.barItems.count > 0 && self.barItems[i].badgeAppearance != nil) { + itemView.badgeAppearance = self.barItems[i].badgeAppearance; + } else { + itemView.badgeAppearance = _itemBadgeAppearance; + } } } - (void)setItemBadgeHorizontalOffset:(CGFloat)itemBadgeHorizontalOffset { _itemBadgeHorizontalOffset = itemBadgeHorizontalOffset; - - for (NSUInteger i = 0; i < self.items.count; ++i) { + for (NSUInteger i = 0; i < [self itemCount]; ++i) { MDCBottomNavigationItemView *itemView = self.itemViews[i]; itemView.badgeHorizontalOffset = itemBadgeHorizontalOffset; } diff --git a/components/BottomNavigation/src/MDCBottomNavigationBarItem.h b/components/BottomNavigation/src/MDCBottomNavigationBarItem.h new file mode 100644 index 00000000000..933bb459053 --- /dev/null +++ b/components/BottomNavigation/src/MDCBottomNavigationBarItem.h @@ -0,0 +1,50 @@ +// Copyright 2024-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 + +#import "MDCBadgeAppearance.h" + +NS_ASSUME_NONNULL_BEGIN + +__attribute__((objc_subclassing_restricted)) +@interface MDCBottomNavigationBarItem : NSObject + +/** + Initializes a new MDCBottomNavigationBarItem with the given UITabBarItem. + */ +- (instancetype)initWithBarItem:(UITabBarItem *)item; + +/** + Initializes a new MDCBottomNavigationBarItem with the given UITabBarItem and MDCBadgeAppearance. + */ +- (instancetype)initWithBarItem:(UITabBarItem *)item + badgeAppearance:(nonnull MDCBadgeAppearance *)badgeAppearance; + +/** + The appearance to be used for this item's badge. Defaults to nil. + + If this property is set to a nil value, the badge will use the default appearance provided to the + MDCBottomNavigationBar.itemBadgeAppearance. + */ +@property(nonatomic, copy, nullable) MDCBadgeAppearance *badgeAppearance; + +/** + A UITabBarItem that is used to populate bottom navigation bar content. + */ +@property(nonatomic, nonnull) UITabBarItem *item; + +@end + +NS_ASSUME_NONNULL_END diff --git a/components/BottomNavigation/src/MDCBottomNavigationBarItem.m b/components/BottomNavigation/src/MDCBottomNavigationBarItem.m new file mode 100644 index 00000000000..c2652380c9a --- /dev/null +++ b/components/BottomNavigation/src/MDCBottomNavigationBarItem.m @@ -0,0 +1,36 @@ + +#import "MDCBottomNavigationBarItem.h" + +#import +#import +#import "MDCBadgeAppearance.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MDCBottomNavigationBarItem () +@end + +@implementation MDCBottomNavigationBarItem + +- (instancetype)initWithBarItem:(UITabBarItem *)item { + self = [super init]; + if (self) { + _item = item; + _badgeAppearance = nil; + } + return self; +} + +- (instancetype)initWithBarItem:(UITabBarItem *)item + badgeAppearance:(nonnull MDCBadgeAppearance *)badgeAppearance { + self = [super init]; + if (self) { + _item = item; + _badgeAppearance = [badgeAppearance copy]; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/components/BottomNavigation/src/private/MDCBottomNavigationItemView.m b/components/BottomNavigation/src/private/MDCBottomNavigationItemView.m index e06aac5779e..e4e7d77e67c 100644 --- a/components/BottomNavigation/src/private/MDCBottomNavigationItemView.m +++ b/components/BottomNavigation/src/private/MDCBottomNavigationItemView.m @@ -948,8 +948,14 @@ - (CGPoint)badgePositionForRTLState:(BOOL)isRTL { if (isRTL) { badgeX = iconX + floor([self iconSize].width * 0.5) - floor([self badgeSize].width) - _badgeHorizontalOffset; + if (_badge.appearance.dotBadgeEnabled) { + badgeX -= 5; + } } else { badgeX = iconX + floor([self iconSize].width * 0.5) + _badgeHorizontalOffset; + if (_badge.appearance.dotBadgeEnabled) { + badgeX += 5; + } } CGFloat badgeY = CGRectGetMinY(indicatorFrame) + kBadgeVerticalOffset; @@ -1087,6 +1093,11 @@ - (void)centerAnchoredLayoutVertical { CGFloat badgeY = badgePosition.y; CGSize badgeSize = [self badgeSize]; CGRect badgeFrame = CGRectIntegral(CGRectMake(badgeX, badgeY, badgeSize.width, badgeSize.height)); + if (_badge.appearance.dotBadgeEnabled) { + CGFloat badgeDiameter = + (_badge.appearance.dotBadgeInnerRadius + _badge.appearance.borderWidth) * 2; + badgeFrame = CGRectMake(badgeX, badgeY, badgeDiameter, badgeDiameter); + } _badge.frame = badgeFrame; CGPoint iconPosition = [self iconPosition]; @@ -1116,6 +1127,11 @@ - (void)centerAnchoredLayoutHorizontal { CGFloat badgeY = badgePosition.y; CGSize badgeSize = [self badgeSize]; CGRect badgeFrame = CGRectIntegral(CGRectMake(badgeX, badgeY, badgeSize.width, badgeSize.height)); + if (_badge.appearance.dotBadgeEnabled) { + CGFloat badgeDiameter = + (_badge.appearance.dotBadgeInnerRadius + _badge.appearance.borderWidth) * 2; + badgeFrame = CGRectMake(badgeX, badgeY, badgeDiameter, badgeDiameter); + } _badge.frame = badgeFrame; CGPoint iconPosition = [self iconPosition];