Skip to content

Instantly share code, notes, and snippets.

@bnickel
Created January 27, 2017 23:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bnickel/a0af46471ca32b6d514b8907bb32f32f to your computer and use it in GitHub Desktop.
Save bnickel/a0af46471ca32b6d514b8907bb32f32f to your computer and use it in GitHub Desktop.
Custom unselected icon colors in UITabBarController
#import <UIKit/UIKit.h>
@interface UITabBarItem (UnselectedTintColor)
@property (nonatomic, copy, nullable, readonly) UIColor *unselectedIconTintColor;
- (void)setUnselectedIconTintColor:(UIColor * _Nullable)unselectedIconTintColor context:(UIViewController *)context;
@end
@import JRSwizzle; // Only dependency. Could easily be replaced.
#import "UITabBarItem+UnselectedTintColor.h"
#import <objc/runtime.h>
dispatch_once_t UITabBar_SEUI_swizzleToken;
BOOL UITabBar_SEUI_wantsCustomTintColors;
@interface UITabBar (UnselectedTintColor)
- (void)_SEUI_updateUnselectedTintColorForTabBarItem:(UITabBarItem *)item;
@end
@interface UIImageView (UnselectedTintColor)
- (void)_SEUI_setTintColorWithTabBarLogic:(UIColor *)color;
@end
@implementation UITabBarItem (UnselectedTintColor)
- (UIColor *)unselectedIconTintColor
{
return objc_getAssociatedObject(self, @selector(unselectedIconTintColor));
}
- (void)setUnselectedIconTintColor:(UIColor *)unselectedIconTintColor context:(UIViewController *)context
{
UITabBar_SEUI_wantsCustomTintColors = YES;
objc_setAssociatedObject(self, @selector(unselectedIconTintColor), unselectedIconTintColor, OBJC_ASSOCIATION_COPY_NONATOMIC);
[context.tabBarController.tabBar _SEUI_updateUnselectedTintColorForTabBarItem:self];
}
@end
@implementation UITabBar (UnselectedTintColor)
+ (void)load
{
[self jr_swizzleMethod:@selector(setItems:animated:) withMethod:@selector(_SEUI_setItems:animated:) error:NULL];
}
/**
Whenever a tab is selected/deselected, UITabBarButton calls setTintColor: on the UIImageView subclass contained within it. We want to swizzle that method so it can choose a better tint color from the UIBarButtonItem.
But... We don't want to override setTintColor: on every image view (that would be expensive) or at all if we don't ever use this. So, we want to swizzle on that subclass only when needed.
*/
- (void)_SEUI_replaceTabBarImageTintColorHook
{
// Fast fail if we haven'd set a color or have already swizzled.
if (!UITabBar_SEUI_wantsCustomTintColors || UITabBar_SEUI_swizzleToken) { return; }
// Fas fail if there are no items.
if (self.items.count == 0) {
return;
}
// Look for the first UIControl and work in there.
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:[UIControl class]]) {
[self _SEUI_replaceTabBarImageTintColorHookInButton:(id)subview];
return;
}
}
}
- (void)_SEUI_replaceTabBarImageTintColorHookInButton:(UIControl *)button
{
// Loop through subviews and swizzle the first view that is a UIImageView subclass. This will break if UIKit stops subclassing or changes the view hierarchy.
for (UIView *subview in button.subviews) {
if ([subview isKindOfClass:[UIImageView class]] && ![subview isMemberOfClass:[UIImageView class]]) {
dispatch_once(&UITabBar_SEUI_swizzleToken, ^{
[[subview class] jr_swizzleMethod:@selector(setTintColor:) withMethod:@selector(_SEUI_setTintColorWithTabBarLogic:) error:NULL];
});
}
}
}
/**
This is called whenever a unselected tint color is set. It finds the matching image view based on the tab's title and forces it to refresh it's color.
*/
- (void)_SEUI_updateUnselectedTintColorForTabBarItem:(UITabBarItem *)item
{
[self _SEUI_replaceTabBarImageTintColorHook];
// Don't update the highlight if not selected.
if (self.selectedItem == item) {
return;
}
for (UIView *subview in self.subviews) {
if ([subview isKindOfClass:[UIControl class]]) {
UIImageView *imageView = nil;
UILabel *label = nil;
for (UIView *subsubview in subview.subviews) {
if ([subsubview isKindOfClass:[UIImageView class]]) {
imageView = (id)subsubview;
} else if ([subsubview isKindOfClass:[UILabel class]]) {
label = (id)subsubview;
}
}
if (item == nil || [item.title isEqualToString:label.text]) {
imageView.tintColor = imageView.tintColor; // That's right, I'm doing it. I don't care.
}
}
}
}
/**
Any time the items are set is a good time to swizzle. It's an infrequent call and pretty much guarantees a button will be there.
*/
- (void)_SEUI_setItems:(NSArray<UITabBarItem *> *)items animated:(BOOL)animated
{
[self _SEUI_replaceTabBarImageTintColorHook];
[self _SEUI_setItems:items animated:animated];
}
@end
@implementation UIImageView (UnselectedTintColor)
/**
Gets the owning button's label and the owning tab bar. Will fail if the label is not a sibling to the button.
*/
- (BOOL)_SEUI_getTabBar:(out UITabBar **)tabBar labelText:(out NSString **)labelText
{
UIView *superview = self.superview;
for (UIView *subview in superview.subviews) {
if ([subview isKindOfClass:[UILabel class]]) {
*labelText = [(UILabel *)subview text];
break;
}
}
if (*labelText == nil) {
return NO;
}
while ((superview = superview.superview)) {
if ([superview isKindOfClass:[UITabBar class]]) {
*tabBar = (id)superview;
return YES;
}
}
return NO;
}
/**
Overrides setTintColor:. When set, the image view get's the button's title and uses that to check that it's not the selected tab. If it's not, it loops through the tab bar items to find a match, again based on title. If the match has an unselectedIconTintColor, that is set, otherwise it calls the original implementation.
*/
- (void)_SEUI_setTintColorWithTabBarLogic:(UIColor *)color
{
UITabBar *tabBar;
NSString *labelText = labelText;
if ([self _SEUI_getTabBar:&tabBar labelText:&labelText] && ![labelText isEqualToString:tabBar.selectedItem.title]) {
for (UITabBarItem *item in tabBar.items) {
if ([labelText isEqualToString:item.title]) {
if (item.unselectedIconTintColor != nil) {
[self _SEUI_setTintColorWithTabBarLogic:item.unselectedIconTintColor];
return;
} else {
break;
}
}
}
}
[self _SEUI_setTintColorWithTabBarLogic:color];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment