适配iOS26自定义UITabBarItem角标和小红点设置
系统自带的其实是够用的, 只是不符合产品需求 所以需要各种灵活的自定义手段~
诸如此类方法 https://huang-libo.github.io/posts/change-UITabBarItem-badge-size/
github一些开源框架都早已年久失修了,无法直接使用,还不如系统的"大胖点"省心省力~
需要魔改系统的一些接口或者增加一些花样, 其实还是挺折腾人的,系统更新可能就伴随着bug的出现, 开发者建议第一条,抛开好不好看不谈,能用系统自带的就用系统自带的吧~
以下代码是借助Gemini 3 Pro preview加持下完成的
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UITabBar (CustomBadge)
/**
显示小圆点(无数字)
@param index TabBarItem的索引 (0, 1, 2...)
@param radius 圆点半径 (建议 4.0)
@param offset 相对图标右上角的偏移量 (x正数向右, y正数向下)
@param color 圆点颜色 (传nil默认红色)
*/
- (void)showRedDotAtIndex:(NSInteger)index
radius:(CGFloat)radius
offset:(CGPoint)offset
color:(nullable UIColor *)color;
/**
显示带文字/数字的角标
@param index TabBarItem的索引
@param text 显示的文字 (如 @"99", @"New")
@param offset 相对图标右上角的偏移量
@param configBlock 用于自定义样式的Block (可设置字体、背景、文字颜色等)
*/
- (void)showBadgeAtIndex:(NSInteger)index
text:(NSString *)text
offset:(CGPoint)offset
configuration:(void (^ _Nullable)(UILabel *badgeLabel))configBlock;
/**
隐藏指定位置的角标
*/
- (void)hideBadgeAtIndex:(NSInteger)index;
/**
[重要] 更新所有角标位置
请在 -viewDidLayoutSubviews 中调用此方法,以适配屏幕旋转或布局变化
*/
- (void)updateBadgePositions;
@end
NS_ASSUME_NONNULL_END
#import "UITabBar+CustomBadge.h"
#import <objc/runtime.h>
// 用于标记角标View的Tag基准值
static NSInteger const kCustomBadgeTagStart = 20000;
// 用于 Runtime 存储 offset 的 Key
static char kBadgeOffsetKey;
@implementation UITabBar (CustomBadge)
#pragma mark - Public Methods
- (void)showRedDotAtIndex:(NSInteger)index radius:(CGFloat)radius offset:(CGPoint)offset color:(UIColor *)color {
[self removeBadgeOnItemIndex:index];
UIView *badgeView = [[UIView alloc] init];
badgeView.tag = kCustomBadgeTagStart + index;
badgeView.layer.cornerRadius = radius;
badgeView.backgroundColor = color ? : [UIColor redColor];
badgeView.frame = CGRectMake(0, 0, radius * 2, radius * 2);
badgeView.userInteractionEnabled = NO; // 防止拦截点击事件
// 保存 offset 属性,以便布局变化时读取
[self setOffset:offset forBadgeView:badgeView];
[self addSubview:badgeView];
[self bringSubviewToFront:badgeView];
// 延迟一下,确保 TabBar 内部布局已完成
dispatch_async(dispatch_get_main_queue(), ^{
[self positionBadgeView:badgeView atIndex:index];
});
}
- (void)showBadgeAtIndex:(NSInteger)index text:(NSString *)text offset:(CGPoint)offset configuration:(void (^)(UILabel * _Nonnull))configBlock {
[self removeBadgeOnItemIndex:index];
UILabel *badgeLabel = [[UILabel alloc] init];
badgeLabel.tag = kCustomBadgeTagStart + index;
badgeLabel.text = text;
badgeLabel.textAlignment = NSTextAlignmentCenter;
badgeLabel.clipsToBounds = YES;
badgeLabel.userInteractionEnabled = NO;
// 默认样式
badgeLabel.backgroundColor = [UIColor redColor];
badgeLabel.textColor = [UIColor whiteColor];
badgeLabel.font = [UIFont systemFontOfSize:10 weight:UIFontWeightMedium];
if (configBlock) {
configBlock(badgeLabel);
}
// 自适应大小并处理圆角
[badgeLabel sizeToFit];
CGRect frame = badgeLabel.frame;
frame.size.width += 8; // 左右内边距
frame.size.height += 4; // 上下内边距
// 保证最小为正圆
if (frame.size.width < frame.size.height) {
frame.size.width = frame.size.height;
}
badgeLabel.frame = frame;
badgeLabel.layer.cornerRadius = frame.size.height / 2.0;
// 保存 offset
[self setOffset:offset forBadgeView:badgeLabel];
[self addSubview:badgeLabel];
[self bringSubviewToFront:badgeLabel];
dispatch_async(dispatch_get_main_queue(), ^{
[self positionBadgeView:badgeLabel atIndex:index];
});
}
- (void)hideBadgeAtIndex:(NSInteger)index {
[self removeBadgeOnItemIndex:index];
}
- (void)updateBadgePositions {
for (UIView *subview in self.subviews) {
if (subview.tag >= kCustomBadgeTagStart) {
NSInteger index = subview.tag - kCustomBadgeTagStart;
[self positionBadgeView:subview atIndex:index];
}
}
}
#pragma mark - Private Helpers
- (void)removeBadgeOnItemIndex:(NSInteger)index {
UIView *badgeView = [self viewWithTag:kCustomBadgeTagStart + index];
if (badgeView) {
[badgeView removeFromSuperview];
}
}
// 核心定位逻辑
- (void)positionBadgeView:(UIView *)view atIndex:(NSInteger)index {
// 1. 获取对应 Index 的 Icon Frame
CGRect iconFrame = [self getIconFrameAtIndex:index];
// 如果没找到(可能 TabBar 还没布局好),暂时不处理
if (CGRectEqualToRect(iconFrame, CGRectZero)) {
return;
}
// 2. 获取保存的 Offset
CGPoint offset = [self getOffsetFromBadgeView:view];
// 3. 计算位置:Icon右上角 + Offset
// iconFrame.maxX 是图标右侧边缘
// iconFrame.minY 是图标顶部边缘
CGFloat x = iconFrame.origin.x + iconFrame.size.width + offset.x;
CGFloat y = iconFrame.origin.y + offset.y;
// 4. 使用 center 定位,并加上 view 自身宽高的一半,使其 anchorPoint 看起来像是左上角或者中心
// 这里我们简单处理:让 view 的中心点 = 计算出的坐标点
view.center = CGPointMake(x, y);
}
#pragma mark - 核心定位逻辑适配 iOS 26+
// 查找系统内部图标的 Frame (适配 iOS 19+ / iOS 26)
- (CGRect)getIconFrameAtIndex:(NSInteger)index {
// 1. 递归查找所有的 TabButton (兼容旧版 UITabBarButton 和新版 _UITabButton)
NSMutableArray<UIView *> *allButtons = [NSMutableArray array];
[self recursiveFindTabButtonsInView:self toArray:allButtons];
// 2. 如果没找到任何按钮,返回空
if (allButtons.count == 0) {
return CGRectZero;
}
// 3. 按照 x 坐标从左到右排序
[allButtons sortUsingComparator:^NSComparisonResult(UIView *view1, UIView *view2) {
// 获取相对 window 或者 tabBar 的 convertRect 后的 x 坐标进行比较更稳妥
CGRect frame1 = [view1 convertRect:view1.bounds toView:self];
CGRect frame2 = [view2 convertRect:view2.bounds toView:self];
return (frame1.origin.x < frame2.origin.x) ? NSOrderedAscending : NSOrderedDescending;
}];
// 4. 【关键步骤】去重逻辑
// iOS 26 可能会有 SelectedContentView 和 ContentView,导致同一个 index 有两个重叠的 _UITabButton
// 我们需要把位置非常接近的按钮视为同一个 Item
NSMutableArray<UIView *> *uniqueButtons = [NSMutableArray array];
UIView *lastButton = nil;
for (UIView *btn in allButtons) {
if (!lastButton) {
[uniqueButtons addObject:btn];
lastButton = btn;
} else {
CGRect lastFrame = [lastButton convertRect:lastButton.bounds toView:self];
CGRect curFrame = [btn convertRect:btn.bounds toView:self];
// 如果两个按钮水平中心点距离很近(例如小于 5pt),认为是同一个 index 的不同状态视图
if (fabs(CGRectGetMidX(lastFrame) - CGRectGetMidX(curFrame)) > 5) {
[uniqueButtons addObject:btn];
lastButton = btn;
} else {
// 如果是重叠的,我们通常倾向于取“层级更高”或者“未隐藏”的那个,
// 但为了计算坐标,只要 Frame 一样,取哪一个都行。
// 这里不做处理,直接跳过。
}
}
}
// 5. 校验 Index 是否越界
if (index < 0 || index >= uniqueButtons.count) {
return CGRectZero;
}
UIView *targetButton = uniqueButtons[index];
// 6. 在目标 Button 内部查找 UIImageView (icon)
UIView *iconView = [self findFirstImageViewInView:targetButton];
// 7. 坐标转换:将 icon 相对 button 的坐标转为相对 tabBar 的坐标
if (iconView) {
// 务必使用 convertRect,因为层级很深
return [iconView convertRect:iconView.bounds toView:self];
} else {
// 兜底方案:如果找不到图,就用 button 中心
CGRect btnFrame = [targetButton convertRect:targetButton.bounds toView:self];
CGFloat w = 24; // 估算图标大小
CGFloat h = 24;
return CGRectMake(CGRectGetMidX(btnFrame) - w/2, CGRectGetMidY(btnFrame) - h/2, w, h);
}
}
#pragma mark - 递归查找辅助方法
// 递归查找所有的 TabButton 容器
- (void)recursiveFindTabButtonsInView:(UIView *)view toArray:(NSMutableArray *)result {
// 过滤隐藏的视图,避免计算干扰
if (view.hidden || view.alpha < 0.01) {
return;
}
NSString *className = NSStringFromClass(view.class);
// 兼容旧版 "UITabBarButton" 和 新版 "_UITabButton"
if ([className isEqualToString:@"UITabBarButton"] ||
[className isEqualToString:@"_UITabButton"]) {
[result addObject:view];
// 找到按钮容器后,通常不需要再往里找子按钮了(避免嵌套导致重复),停止当前分支递归
return;
}
// 特殊情况:如果按照你说的 _UITabButton 里面不再包含按钮,而是图片和文字,上面的逻辑是没问题的。
// 继续遍历子视图
for (UIView *subview in view.subviews) {
[self recursiveFindTabButtonsInView:subview toArray:result];
}
}
// 递归查找 Button 内部的 ImageView
- (UIImageView *)findFirstImageViewInView:(UIView *)view {
// 过滤掉非图标的 Image,比如背景图或阴影
// 通常 Icon 的尺寸较小,且具备 userInteractionEnabled = NO 等特征
// 这里简单判断:如果是 UIImageView 且 size 适中
if ([view isKindOfClass:[UIImageView class]]) {
// 有些系统背景图是 UIImageView,需要过滤。
// 经验判断:TabIcon 宽度通常在 20-40 之间,高度类似
// 如果你的 TabBar 自定义了图片大小,这里可能需要调整
if (view.bounds.size.width > 10 && view.bounds.size.height > 10) {
return (UIImageView *)view;
}
}
for (UIView *subview in view.subviews) {
UIImageView *img = [self findFirstImageViewInView:subview];
if (img) {
return img;
}
}
return nil;
}
#pragma mark - Runtime Associated Objects
- (void)setOffset:(CGPoint)offset forBadgeView:(UIView *)view {
NSValue *offsetValue = [NSValue valueWithCGPoint:offset];
objc_setAssociatedObject(view, &kBadgeOffsetKey, offsetValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGPoint)getOffsetFromBadgeView:(UIView *)view {
NSValue *offsetValue = objc_getAssociatedObject(view, &kBadgeOffsetKey);
return offsetValue ? [offsetValue CGPointValue] : CGPointZero;
}
@end
使用方法如下:
- (void)setupBadges {
// 场景1:首页图标很大,红点需要往右上角偏移一点,不要压住图标
[self.tabBar showRedDotAtIndex:0
radius:4.0
offset:CGPointMake(-3, 3) // 向左3,向下3(因为y向下是正,所以负数是向上)
color:nil];
// 场景2:消息图标很小,红点需要往左下缩进一点,紧贴着
[self.tabBar showBadgeAtIndex:1
text:@"99+"
offset:CGPointMake(3, 3) // 向左3,向下3
configuration:^(UILabel * _Nonnull badgeLabel) {
badgeLabel.backgroundColor = [UIColor colorWithRed:1.0 green:0.3 blue:0.3 alpha:1];
badgeLabel.font = [UIFont systemFontOfSize:9 weight:UIFontWeightBold];
}];
}
未经作者授权,禁止转载
本文来自博客园,作者:CoderWGB,转载请注明原文链接:https://www.cnblogs.com/wgb1234/p/19254494
THE END

浙公网安备 33010602011771号