From 26d3d8219097fe1b22a9585526ab8a712af6a508 Mon Sep 17 00:00:00 2001 From: Yogev Ben David Date: Sun, 24 Nov 2019 16:51:06 +0200 Subject: [PATCH] Support centered bottomTab icons using bottomTabs.titleDisplayMode option (#5667) * Support centered bottomTab icons using bottomTabs.titleDisplayMode option * Improve readability, better namings * Rename setTabItemImagesCentered to centerTabItems --- lib/ios/RNNBottomTabsOptions.h | 1 + lib/ios/RNNBottomTabsOptions.m | 1 + lib/ios/RNNBottomTabsPresenter.m | 3 + .../project.pbxproj | 6 ++ .../RNNTabBarPresenterTest.m | 16 +++++ .../UITabBarController+RNNOptionsTest.m | 10 +++- lib/ios/UITabBar+utils.h | 7 +++ lib/ios/UITabBar+utils.m | 60 +++++++++++++++++++ lib/ios/UITabBarController+RNNOptions.h | 2 + lib/ios/UITabBarController+RNNOptions.m | 5 ++ 10 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 lib/ios/UITabBar+utils.h create mode 100644 lib/ios/UITabBar+utils.m diff --git a/lib/ios/RNNBottomTabsOptions.h b/lib/ios/RNNBottomTabsOptions.h index a6b1027c43f..2c39daf8dc8 100644 --- a/lib/ios/RNNBottomTabsOptions.h +++ b/lib/ios/RNNBottomTabsOptions.h @@ -17,5 +17,6 @@ @property (nonatomic, strong) Text* currentTabId; @property (nonatomic, strong) Text* barStyle; @property (nonatomic, strong) Text* fontFamily; +@property (nonatomic, strong) Text* titleDisplayMode; @end diff --git a/lib/ios/RNNBottomTabsOptions.m b/lib/ios/RNNBottomTabsOptions.m index cb040159c27..9478a0e4d81 100644 --- a/lib/ios/RNNBottomTabsOptions.m +++ b/lib/ios/RNNBottomTabsOptions.m @@ -20,6 +20,7 @@ - (instancetype)initWithDict:(NSDictionary *)dict { self.currentTabId = [TextParser parse:dict key:@"currentTabId"]; self.barStyle = [TextParser parse:dict key:@"barStyle"]; self.fontFamily = [TextParser parse:dict key:@"fontFamily"]; + self.titleDisplayMode = [TextParser parse:dict key:@"titleDisplayMode"]; return self; } diff --git a/lib/ios/RNNBottomTabsPresenter.m b/lib/ios/RNNBottomTabsPresenter.m index 6ca11edef25..790b7b12c9a 100644 --- a/lib/ios/RNNBottomTabsPresenter.m +++ b/lib/ios/RNNBottomTabsPresenter.m @@ -9,6 +9,9 @@ - (void)applyOptionsOnInit:(RNNNavigationOptions *)options { UITabBarController *bottomTabs = self.boundViewController; RNNNavigationOptions *withDefault = [options withDefault:[self defaultOptions]]; [bottomTabs setCurrentTabIndex:[withDefault.bottomTabs.currentTabIndex getWithDefaultValue:0]]; + if ([[withDefault.bottomTabs.titleDisplayMode getWithDefaultValue:@"alwaysShow"] isEqualToString:@"alwaysHide"]) { + [bottomTabs centerTabItems]; + } } - (void)applyOptions:(RNNNavigationOptions *)options { diff --git a/lib/ios/ReactNativeNavigation.xcodeproj/project.pbxproj b/lib/ios/ReactNativeNavigation.xcodeproj/project.pbxproj index dcf99e149fb..4455328e10a 100644 --- a/lib/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +++ b/lib/ios/ReactNativeNavigation.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 390AD478200F499D00A8250D /* RNNSwizzles.m in Sources */ = {isa = PBXBuildFile; fileRef = 390AD476200F499D00A8250D /* RNNSwizzles.m */; }; 4534E72520CB6724009F8185 /* RNNLargeTitleOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 4534E72320CB6724009F8185 /* RNNLargeTitleOptions.h */; }; 4534E72620CB6724009F8185 /* RNNLargeTitleOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 4534E72420CB6724009F8185 /* RNNLargeTitleOptions.m */; }; + 5008641223856A2D00A55BE9 /* UITabBar+utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5008641023856A2C00A55BE9 /* UITabBar+utils.m */; }; 501214C9217741A000435148 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 501214C8217741A000435148 /* libOCMock.a */; }; 501223D72173590F000F5F98 /* RNNStackPresenter.h in Headers */ = {isa = PBXBuildFile; fileRef = 501223D52173590F000F5F98 /* RNNStackPresenter.h */; }; 501223D82173590F000F5F98 /* RNNStackPresenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 501223D62173590F000F5F98 /* RNNStackPresenter.m */; }; @@ -399,6 +400,8 @@ 390AD476200F499D00A8250D /* RNNSwizzles.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNNSwizzles.m; sourceTree = ""; }; 4534E72320CB6724009F8185 /* RNNLargeTitleOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNNLargeTitleOptions.h; sourceTree = ""; }; 4534E72420CB6724009F8185 /* RNNLargeTitleOptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNLargeTitleOptions.m; sourceTree = ""; }; + 5008641023856A2C00A55BE9 /* UITabBar+utils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UITabBar+utils.m"; sourceTree = ""; }; + 5008641123856A2D00A55BE9 /* UITabBar+utils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UITabBar+utils.h"; sourceTree = ""; }; 501214C8217741A000435148 /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libOCMock.a; sourceTree = ""; }; 501223D52173590F000F5F98 /* RNNStackPresenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNNStackPresenter.h; sourceTree = ""; }; 501223D62173590F000F5F98 /* RNNStackPresenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNNStackPresenter.m; sourceTree = ""; }; @@ -733,6 +736,8 @@ 50706E6C20CE7CA5003345C3 /* UIImage+tint.m */, 506317A8220B547400B26FC3 /* UIImage+insets.h */, 506317A9220B547400B26FC3 /* UIImage+insets.m */, + 5008641123856A2D00A55BE9 /* UITabBar+utils.h */, + 5008641023856A2C00A55BE9 /* UITabBar+utils.m */, 5038A372216CDDB6009280BC /* UIViewController+SideMenuController.h */, 5038A373216CDDB6009280BC /* UIViewController+SideMenuController.m */, 5038A3BF216E1E66009280BC /* RNNFontAttributesCreator.h */, @@ -1604,6 +1609,7 @@ 501224072173592D000F5F98 /* RNNBottomTabsPresenter.m in Sources */, 50A00C38200F84D6000F01A6 /* RNNOverlayOptions.m in Sources */, 5039559C2174867000B0A663 /* DoubleParser.m in Sources */, + 5008641223856A2D00A55BE9 /* UITabBar+utils.m in Sources */, E5F6C3A822DB4D0F0093C2CE /* UIView+Utils.m in Sources */, 5049593F216F5D73006D2B81 /* BoolParser.m in Sources */, 50EB93421FE14A3E00BD8EEE /* RNNBottomTabOptions.m in Sources */, diff --git a/lib/ios/ReactNativeNavigationTests/RNNTabBarPresenterTest.m b/lib/ios/ReactNativeNavigationTests/RNNTabBarPresenterTest.m index 37cda8972a4..eeb899413d1 100644 --- a/lib/ios/ReactNativeNavigationTests/RNNTabBarPresenterTest.m +++ b/lib/ios/ReactNativeNavigationTests/RNNTabBarPresenterTest.m @@ -54,6 +54,22 @@ - (void)testApplyOptions_shouldApplyOptions { [self.boundViewController verify]; } +- (void)testApplyOptionsOnInit_alwaysShow_shouldNotCenterTabImages { + RNNNavigationOptions *initialOptions = [[RNNNavigationOptions alloc] initEmptyOptions]; + initialOptions.bottomTabs.titleDisplayMode = [[Text alloc] initWithValue:@"alwaysShow"]; + [[self.boundViewController reject] centerTabItems]; + [self.uut applyOptionsOnInit:initialOptions]; + [self.boundViewController verify]; +} + +- (void)testApplyOptions_shouldApplyOptionsOnInit_alwaysHide_shouldCenterTabImages { + RNNNavigationOptions *initialOptions = [[RNNNavigationOptions alloc] initEmptyOptions]; + initialOptions.bottomTabs.titleDisplayMode = [[Text alloc] initWithValue:@"alwaysHide"]; + [[self.boundViewController expect] centerTabItems]; + [self.uut applyOptionsOnInit:initialOptions]; + [self.boundViewController verify]; +} + - (void)testViewDidLayoutSubviews_appliesBadgeOnNextRunLoop { id uut = [self uut]; [[uut expect] applyDotIndicator]; diff --git a/lib/ios/ReactNativeNavigationTests/UITabBarController+RNNOptionsTest.m b/lib/ios/ReactNativeNavigationTests/UITabBarController+RNNOptionsTest.m index 7018286ef26..35fdef02af9 100644 --- a/lib/ios/ReactNativeNavigationTests/UITabBarController+RNNOptionsTest.m +++ b/lib/ios/ReactNativeNavigationTests/UITabBarController+RNNOptionsTest.m @@ -1,6 +1,7 @@ #import -#import +#import #import "UITabBarController+RNNOptions.h" +#import "UITabBar+utils.h" @interface UITabBarController_RNNOptionsTest : XCTestCase @@ -13,6 +14,7 @@ @implementation UITabBarController_RNNOptionsTest - (void)setUp { [super setUp]; self.uut = [OCMockObject partialMockForObject:[UITabBarController new]]; + OCMStub([self.uut tabBar]).andReturn([OCMockObject partialMockForObject:[UITabBar new]]); } - (void)test_tabBarTranslucent_true { @@ -39,6 +41,12 @@ - (void)test_tabBarHideShadow_false { XCTAssertFalse(self.uut.tabBar.clipsToBounds); } +- (void)test_centerTabItems { + [[(id)self.uut.tabBar expect] centerTabItems]; + [self.uut centerTabItems]; + [(id)self.uut.tabBar verify]; +} + - (void)test_tabBarBackgroundColor { UIColor* tabBarBackgroundColor = [UIColor redColor]; diff --git a/lib/ios/UITabBar+utils.h b/lib/ios/UITabBar+utils.h new file mode 100644 index 00000000000..85649a9e2ea --- /dev/null +++ b/lib/ios/UITabBar+utils.h @@ -0,0 +1,7 @@ +#import + +@interface UITabBar (utils) + +- (void)centerTabItems; + +@end diff --git a/lib/ios/UITabBar+utils.m b/lib/ios/UITabBar+utils.m new file mode 100644 index 00000000000..1cc04220b4d --- /dev/null +++ b/lib/ios/UITabBar+utils.m @@ -0,0 +1,60 @@ +#import "UITabBar+utils.h" +#import + +#define BADGE_OFFSET 0.2 +#define IMAGE_VIEW_TAG 1 + +typedef void (*UITabBarButton_layoutSubviews__IMP)(void); +static UITabBarButton_layoutSubviews__IMP original_UITabBarButton_layoutSubviews; + +@implementation UITabBar (utils) + +- (void)centerTabItems { + [self removeTabBarItemTitles]; + [self swizzleUITabBarButton]; +} + +- (void)removeTabBarItemTitles { + for (UITabBarItem* item in self.items) { + item.title = nil; + } +} + +- (void)swizzleUITabBarButton { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [[self class] swizzleUITabBarButtonLayoutSubviews]; + }); +} + ++ (void)swizzleUITabBarButtonLayoutSubviews { + Class UITabBarButtonClass = NSClassFromString(@"UITabBarButton"); + + SEL layoutSubviewsSEL = @selector(layoutSubviews); + Method layoutSubviewsMethod = class_getInstanceMethod(UITabBarButtonClass, layoutSubviewsSEL); + IMP layoutSubviewsIMP = method_getImplementation(layoutSubviewsMethod); + + original_UITabBarButton_layoutSubviews = layoutSubviewsIMP; + + SEL swizzleUITabBarButton_layoutSubviewsSEL = @selector(swizzleUITabBarButton_layoutSubviews); + Method swizzleUITabBarButton_layoutSubviewsMethod = class_getInstanceMethod(self, swizzleUITabBarButton_layoutSubviewsSEL); + + method_exchangeImplementations(layoutSubviewsMethod, swizzleUITabBarButton_layoutSubviewsMethod); +} + +- (void)swizzleUITabBarButton_layoutSubviews { + original_UITabBarButton_layoutSubviews(); + for (UIView *subView in self.subviews) { + if ([subView isKindOfClass:NSClassFromString(@"UITabBarSwappableImageView")]) { + subView.center = CGPointMake(subView.center.x, subView.superview.frame.size.height / 2); + subView.tag = IMAGE_VIEW_TAG; + } + + if ([subView isKindOfClass:NSClassFromString(@"_UIBadgeView")]) { + UIView* imageView = [subView.superview viewWithTag:IMAGE_VIEW_TAG]; + subView.frame = CGRectMake(subView.frame.origin.x, (imageView.frame.origin.y + imageView.frame.size.height * BADGE_OFFSET) - subView.frame.size.height / 2, subView.frame.size.width, subView.frame.size.height); + } + } +} + +@end diff --git a/lib/ios/UITabBarController+RNNOptions.h b/lib/ios/UITabBarController+RNNOptions.h index 412bb07d9ec..f1d09c94fc5 100644 --- a/lib/ios/UITabBarController+RNNOptions.h +++ b/lib/ios/UITabBarController+RNNOptions.h @@ -18,4 +18,6 @@ - (void)setTabBarVisible:(BOOL)visible animated:(BOOL)animated; +- (void)centerTabItems; + @end diff --git a/lib/ios/UITabBarController+RNNOptions.m b/lib/ios/UITabBarController+RNNOptions.m index d95f7978b4a..5af6788d141 100644 --- a/lib/ios/UITabBarController+RNNOptions.m +++ b/lib/ios/UITabBarController+RNNOptions.m @@ -1,5 +1,6 @@ #import "UITabBarController+RNNOptions.h" #import "RNNBottomTabsController.h" +#import "UITabBar+utils.h" @implementation UITabBarController (RNNOptions) @@ -74,6 +75,10 @@ - (void)setTabBarVisible:(BOOL)visible animated:(BOOL)animated { } } +- (void)centerTabItems { + [self.tabBar centerTabItems]; +} + - (void)forEachTab:(void (^)(UIView *, UIViewController * tabViewController, int tabIndex))performOnTab { int tabIndex = 0; for (UIView * tab in self.tabBar.subviews) {