iOS 26 适配 | 使用 `hidesSharedBackground` 保持导航栏按钮原有样式

1 阅读5分钟

iOS 26 适配 | 使用 hidesSharedBackground 保持导航栏按钮原有样式

背景

iOS 26 引入了全新的液态玻璃(Liquid Glass)设计语言,导航栏按钮的默认视觉风格发生了较大变化——多个按钮会被合并在一个统一的玻璃背景块中展示。对于希望在 iOS 26 下保持 iOS 26 之前导航栏按钮样式的开发者来说,苹果提供了 hidesSharedBackground API,用于将共享背景拆分,让每个 item 拥有独立的 Liquid Glass 背景:

if (@available(iOS 26.0, *)) {
    item.hidesSharedBackground = YES;
}

启用后,每个 item 的玻璃背景块会被单独渲染,视觉上更接近旧版导航栏中按钮各自独立的呈现方式。但问题随之而来:系统会在每个玻璃背景块之间插入默认间距,开发者无法通过常规 API 将这个间距收紧为 0,导致多个按钮之间出现明显的视觉割裂感,与 iOS 26 之前的紧凑排列效果存在差异。

因此,仅设置 hidesSharedBackground = YES 还不够,还需要额外处理 PlatterView 的间距问题,才能真正还原旧版导航栏的按钮布局样式。


问题根因分析

在 iOS 26 中,每个 UIBarButtonItem 的 Liquid Glass 背景块由私有容器 _UINavigationBarPlatterView 承载。

UINavigationBar
  └── _UINavigationBarContentView
        ├── _UINavigationBarPlatterView   ← 左侧按钮容器(含独立玻璃背景)
        │     └── _UIButtonBarButton
        └── _UINavigationBarPlatterView   ← 右侧按钮容器(含独立玻璃背景)
              └── _UIButtonBarButton

每个 PlatterView 负责绘制该按钮的 Liquid Glass 背景块,同时也决定了按钮在导航栏中的排列位置。系统在计算这些容器的布局时,会在相邻 PlatterView 之间注入固定的默认间距,且这个间距:

  • 无法通过 UIBarButtonSystemItemFixedSpace 负间距消除(iOS 26 已失效)
  • 无法通过修改 customView 的约束影响
  • 无法通过 UINavigationBar 的公开布局 API 干预

解决方案

核心思路:在布局完成后,运行时递归查找所有 PlatterView 容器,强制重置其 x 坐标与 Leading 约束,将相邻玻璃背景块之间的间距收紧为 0,从而还原 iOS 26 之前导航栏按钮的紧凑排列效果。

完整代码

#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 末尾触发:

- (void)layoutSubviews {
    [super layoutSubviews];
    
    if (@available(iOS 26.0, *)) {
        [self fixPlatterViewSpace];
    }
}

逻辑拆解

1. 递归收集 PlatterView

[self collectPlatterViews:self result:platterViews];

使用类名字符串匹配 PlatterView,而非直接引用私有类,规避了编译报错。找到 PlatterView 后立即收集,不再递归其子视图,防止嵌套层级的重复收集。

2. 以中线划分左右语义区

CGFloat midX = navBarWidth / 2.0;

导航栏天然地以中线分隔 leftBarButtonItemsrightBarButtonItems 的语义区域,以此作为分组依据,保证左右按钮的 PlatterView 不会被错误归类。

3. 左侧从 x=0 紧密排列

leftX = 0
[BackButton]x = 0
[OtherButton]x = BackButton.width

从导航栏左侧起点开始,将各 PlatterView 依次紧贴排列,彻底消除相邻玻璃背景块之间的系统默认间距,还原旧版左侧按钮的紧凑布局。

4. 右侧从右边缘留 5pt 向左排列

rightX = navBarWidth - 5
[Button2] → rightX -= Button2.width
[Button1] → rightX -= Button1.width

保留 5pt 右侧安全边距,确保最右侧玻璃背景块不会贴边,同时各 PlatterView 之间零间距紧密排布,与旧版右侧按钮排列保持一致。

5. 约束修改 + frame 双保险

// 先改约束(正确路径)
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 并强制重置间距,将按钮排列收紧为旧版的紧凑样式。两步缺一不可。