iOS 26 适配 | 使用 hidesSharedBackground 保持导航栏按钮原有样式
背景
iOS 26 引入了全新的液态玻璃(Liquid Glass)设计语言,导航栏按钮的默认视觉风格发生了较大变化------多个按钮会被合并在一个统一的玻璃背景块中展示。对于希望在 iOS 26 下保持 iOS 26 之前导航栏按钮样式 的开发者来说,苹果提供了 hidesSharedBackground API,用于将共享背景拆分,让每个 item 拥有独立的 Liquid Glass 背景:
objc
if (@available(iOS 26.0, *)) {
item.hidesSharedBackground = YES;
}
启用后,每个 item 的玻璃背景块会被单独渲染,视觉上更接近旧版导航栏中按钮各自独立的呈现方式。但问题随之而来:系统会在每个玻璃背景块之间插入默认间距,开发者无法通过常规 API 将这个间距收紧为 0,导致多个按钮之间出现明显的视觉割裂感,与 iOS 26 之前的紧凑排列效果存在差异。
因此,仅设置 hidesSharedBackground = YES 还不够,还需要额外处理 PlatterView 的间距问题,才能真正还原旧版导航栏的按钮布局样式。
问题根因分析
在 iOS 26 中,每个 UIBarButtonItem 的 Liquid Glass 背景块由私有容器 _UINavigationBarPlatterView 承载。
markdown
UINavigationBar
└── _UINavigationBarContentView
├── _UINavigationBarPlatterView ← 左侧按钮容器(含独立玻璃背景)
│ └── _UIButtonBarButton
└── _UINavigationBarPlatterView ← 右侧按钮容器(含独立玻璃背景)
└── _UIButtonBarButton
每个 PlatterView 负责绘制该按钮的 Liquid Glass 背景块,同时也决定了按钮在导航栏中的排列位置。系统在计算这些容器的布局时,会在相邻 PlatterView 之间注入固定的默认间距,且这个间距:
- 无法通过
UIBarButtonSystemItemFixedSpace负间距消除(iOS 26 已失效) - 无法通过修改
customView的约束影响 - 无法通过
UINavigationBar的公开布局 API 干预
解决方案
核心思路:在布局完成后,运行时递归查找所有 PlatterView 容器,强制重置其 x 坐标与 Leading 约束,将相邻玻璃背景块之间的间距收紧为 0,从而还原 iOS 26 之前导航栏按钮的紧凑排列效果。
完整代码
objc
#pragma mark - iOS 26 PlatterView 间距修复
- (void)fixPlatterViewSpace {
// 收集所有 PlatterView
NSMutableArray<UIView *> *platterViews = [NSMutableArray array];
[self collectPlatterViews:self result:platterViews];
if (platterViews.count == 0) return;
CGFloat navBarWidth = self.frame.size.width;
CGFloat midX = navBarWidth / 2.0;
// 按中心点分左右
NSMutableArray *leftViews = [NSMutableArray array];
NSMutableArray *rightViews = [NSMutableArray array];
for (UIView *v in platterViews) {
CGFloat centerX = v.frame.origin.x + v.frame.size.width / 2.0;
if (centerX < midX) {
[leftViews addObject:v];
} else {
[rightViews addObject:v];
}
}
// 左侧:按 x 升序,从 0 开始依次排列
[leftViews sortUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
return a.frame.origin.x > b.frame.origin.x
? NSOrderedDescending : NSOrderedAscending;
}];
CGFloat leftX = 0;
for (UIView *v in leftViews) {
[self fixPlatterView:v toX:leftX];
leftX += v.frame.size.width;
}
// 右侧:按 x 降序,从右边缘 -5 开始向左排列
[rightViews sortUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
return a.frame.origin.x < b.frame.origin.x
? NSOrderedDescending : NSOrderedAscending;
}];
CGFloat rightX = navBarWidth - 5;
for (UIView *v in rightViews) {
rightX -= v.frame.size.width;
[self fixPlatterView:v toX:rightX];
}
}
- (void)collectPlatterViews:(UIView *)view result:(NSMutableArray *)result {
for (UIView *subview in view.subviews) {
if ([NSStringFromClass(subview.class) containsString:@"PlatterView"]) {
[result addObject:subview];
} else {
[self collectPlatterViews:subview result:result];
}
}
}
- (void)fixPlatterView:(UIView *)platterView toX:(CGFloat)x {
// 优先修改约束
for (NSLayoutConstraint *constraint in platterView.superview.constraints) {
if (constraint.firstItem == platterView &&
constraint.firstAttribute == NSLayoutAttributeLeading) {
constraint.constant = x;
}
}
// frame 兜底
CGRect frame = platterView.frame;
frame.origin.x = x;
platterView.frame = frame;
}
调用时机
该方法需要在UINavigationBar布局完成后调用,推荐在 layoutSubviews 末尾触发:
objc
- (void)layoutSubviews {
[super layoutSubviews];
if (@available(iOS 26.0, *)) {
[self fixPlatterViewSpace];
}
}
逻辑拆解
1. 递归收集 PlatterView
objc
[self collectPlatterViews:self result:platterViews];
使用类名字符串匹配 PlatterView,而非直接引用私有类,规避了编译报错。找到 PlatterView 后立即收集,不再递归其子视图,防止嵌套层级的重复收集。
2. 以中线划分左右语义区
objc
CGFloat midX = navBarWidth / 2.0;
导航栏天然地以中线分隔 leftBarButtonItems 和 rightBarButtonItems 的语义区域,以此作为分组依据,保证左右按钮的 PlatterView 不会被错误归类。
3. 左侧从 x=0 紧密排列
ini
leftX = 0
[BackButton] → x = 0
[OtherButton] → x = BackButton.width
从导航栏左侧起点开始,将各 PlatterView 依次紧贴排列,彻底消除相邻玻璃背景块之间的系统默认间距,还原旧版左侧按钮的紧凑布局。
4. 右侧从右边缘留 5pt 向左排列
ini
rightX = navBarWidth - 5
[Button2] → rightX -= Button2.width
[Button1] → rightX -= Button1.width
保留 5pt 右侧安全边距,确保最右侧玻璃背景块不会贴边,同时各 PlatterView 之间零间距紧密排布,与旧版右侧按钮排列保持一致。
5. 约束修改 + frame 双保险
objc
// 先改约束(正确路径)
constraint.constant = x;
// 再改 frame(兜底)
platterView.frame = frame;
优先走 Auto Layout 路径修改 Leading 约束保证一致性,frame 赋值作为兜底,确保在纯 frame 布局场景下同样生效。
注意事项
| 事项 | 说明 |
|---|---|
| 仅限 iOS 26+ | 用 @available(iOS 26.0, *) 包裹调用,避免影响低版本行为 |
| 调用时机 | 必须在 layoutSubviews 之后,frame 确定后才能正确分组 |
| Safe Area | 左侧从 x=0 起排,刘海屏 / Dynamic Island 下需结合 safeAreaInsets.left 调整起始偏移 |
| 私有类名风险 | 依赖类名包含 PlatterView 的字符串匹配,若苹果后续改名则需同步更新 |
| 约束冲突 | 当前仅修改 Leading 约束;若 PlatterView 同时存在 Trailing / Center 约束,可能引发冲突,需一并处理 |
小结
iOS 26 的 Liquid Glass 设计语言改变了导航栏按钮的默认视觉风格。对于需要在 iOS 26 下维持旧版导航栏样式的项目,完整的适配路径分为两步:第一步通过 hidesSharedBackground = YES 拆分共享玻璃背景,让每个 item 独立渲染;第二步通过运行时遍历 PlatterView 并强制重置间距,将按钮排列收紧为旧版的紧凑样式。两步缺一不可。