用 Xcode 27(iOS 27 SDK)重新编译同一套 UIKit 工程后,很多团队会遇到同一种「似曾相识」的不适感:
- 导航栏右上角多个按钮被拆成一串玻璃圆,间距突然拉开
- 搜索框像没了底,白茫茫一片分不清输入区域
- 自定义顶栏顶进状态栏,和系统时间、电量叠在一起
- Info.plist 里写了
UIDesignRequiresCompatibility = YES,结果完全不管用
这不是错觉。Apple 已经写明:用 iOS 27 SDK 构建时,兼容开关会被忽略,Liquid Glass 相关表现会强制进来。本文不讲 Scene 迁移和提审清单(那是另一篇文章),只聚焦一件事——导航栏、搜索栏、自定义顶栏怎么在「去不掉玻璃」的前提下,把体验拉回接近 iOS 26 的可用状态。
文中代码可直接改类名复用,不绑定任何具体 App。
先认清三件事
1. 兼容开关救不了 Xcode 27 包
UIDesignRequiresCompatibility 在「用旧 SDK 打的包跑新系统」时还有意义;一旦换成 Xcode 27 出包,系统会忽略它。继续指望 plist 挡 Liquid Glass,会耽误排期。
2. 导航栏变丑,往往不是「高度从 44 变成 54」这么简单
有人一上来就 swizzle sizeThatFits、在 layoutSubviews 里改 frame,想把导航栏硬压回 44。我们踩过坑:
| 做法 | 真实结果 |
|---|---|
layoutSubviews 改高度 / 约束 | 布局死循环,启动内存暴涨到数 GB(OOM) |
全局 sizeThatFits 强制 44 | top safe area 算乱,内容顶进状态栏 |
结论:别用改系统导航栏高度去「对齐旧版」。 高度交给系统;需要对齐的是观感(不透明、按钮间距、搜索底)和自定义顶栏的 safe area。
3. 首页若自己画了一套顶栏,问题在宏,不在 UINavigationBar
很多 IM / 消息首页会 hidden 掉系统导航栏,自己用 statusBarHeight + 44 拼一块自定义栏。iOS 26 多 Scene 下,启动早期 statusBarFrame 经常是 0,于是顶栏只有 44pt 贴在屏幕最顶上——看起来像「飘进状态栏」。这和是否强压系统导航栏高度是两条线,别搅在一起。
一、先把导航栏「实」回来:Opaque Appearance
Liquid Glass 下,半透明 / effect 会把标题和按钮冲得发虚。基类导航控制器里统一不透明即可:
static void ApplyOpaqueNavBarAppearance(UINavigationBar *navBar,
UIColor *backgroundColor,
UIColor *titleColor) {
if (@available(iOS 13.0, *)) {
UINavigationBarAppearance *appearance = [UINavigationBarAppearance new];
[appearance configureWithOpaqueBackground];
appearance.backgroundEffect = nil;
appearance.backgroundColor = backgroundColor;
appearance.titleTextAttributes = @{
NSForegroundColorAttributeName : titleColor ?: UIColor.whiteColor
};
appearance.shadowColor = UIColor.clearColor;
appearance.shadowImage = [UIImage new];
navBar.standardAppearance = appearance;
navBar.scrollEdgeAppearance = appearance;
if (@available(iOS 15.0, *)) {
navBar.compactAppearance = appearance;
navBar.compactScrollEdgeAppearance = appearance;
}
}
navBar.translucent = NO;
navBar.barTintColor = backgroundColor;
navBar.backgroundColor = backgroundColor;
}
IM 栈若另有一套 UINavigationController 子类,同样在 viewWillAppear 刷一遍,避免「主工程 opaque、IM 栈仍玻璃」。
单按钮去共享玻璃底(iOS 26+):
if (@available(iOS 26.0, *)) {
item.hidesSharedBackground = YES;
item.sharesBackground = NO;
}
注意:只对「每一个」item 开 hidesSharedBackground,多按钮时反而会间距爆炸——下一节解决。
二、多按钮间距爆炸:合并成一个 Stack
现象:聊天页右上角「通话 + 更多」等,在 iOS 27 上中间空出一大块,像两个独立玻璃球。
原因可以简化理解:系统按「各自一块共享背景」排布;关掉共享背景后,旧版那种「挤在一起」的视觉就不在了。
稳定做法是:多于一个 item 时,提成 UIView,塞进 UIStackView,再包成单个 UIBarButtonItem。间距自己控(例如 12pt)。
+ (NSArray<UIBarButtonItem *> *)compactItems:(NSArray<UIBarButtonItem *> *)items
isRightSide:(BOOL)isRightSide {
if ([NSProcessInfo processInfo].operatingSystemVersion.majorVersion < 26) {
return items;
}
if (items.count <= 1) {
return items;
}
NSMutableArray<UIView *> *views = [NSMutableArray array];
for (UIBarButtonItem *item in items) {
// FixedSpace / 空占位跳过;customView 优先,否则用 UIButton 包一层 image/title
UIView *v = ViewFromBarButtonItem(item);
if (!v) { return items; } // 抽不出来就别硬合并,避免弄坏系统项
[views addObject:v];
}
if (views.count <= 1) { return items; }
// rightBarButtonItems:系统约定 index0 在最右侧,Stack 从左到右要 reverse
NSArray<UIView *> *arranged = isRightSide
? [[views reverseObjectEnumerator] allObjects]
: [views copy];
UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:arranged];
stack.axis = UILayoutConstraintAxisHorizontal;
stack.alignment = UIStackViewAlignmentCenter;
stack.spacing = 12.0;
stack.tag = 3092601; // 防重复合并
CGFloat totalW = /* 各子视图宽度 + spacing */;
stack.frame = CGRectMake(0, 0, totalW, 44.0);
UIBarButtonItem *one = [[UIBarButtonItem alloc] initWithCustomView:stack];
if (@available(iOS 26.0, *)) {
one.hidesSharedBackground = YES;
one.sharesBackground = NO;
}
return @[ one ];
}
落地时建议三层兜底,避免业务在 viewDidLoad / push 前后多次赋值漏网:
- Swizzle
UINavigationItem的setLeft/RightBarButtonItem(s): - 导航控制器
willShow/didShow/push再压一次 - 基类 VC
viewWillAppear补一次
这样「聊天页单独合并」和「全局多按钮」可以共用一套逻辑。
三、自定义顶栏顶进状态栏:信 safeArea,别信启动瞬间的 0
典型错误
// 启动早期经常拿到 0
CGFloat h = statusBarFrameHeight + 44.0;
[customNavBar.heightAnchor constraintEqualToConstant:h].active = YES;
更稳的状态栏高度
+ (CGFloat)statusBarHeight {
CGFloat height = 0;
if (@available(iOS 13.0, *)) {
UIWindowScene *scene = nil;
for (UIScene *s in UIApplication.sharedApplication.connectedScenes) {
if (![s isKindOfClass:UIWindowScene.class]) { continue; }
UIWindowScene *ws = (UIWindowScene *)s;
if (ws.activationState == UISceneActivationStateForegroundActive) {
scene = ws; break;
}
if (!scene) { scene = ws; }
}
height = scene.statusBarManager.statusBarFrame.size.height;
if (height < 1) {
for (UIWindow *w in scene.windows) {
if (w.isKeyWindow || w.windowLevel == UIWindowLevelNormal) {
height = w.safeAreaInsets.top;
if (height > 1) { break; }
}
}
}
}
if (height < 1) {
height = UIApplication.sharedApplication.statusBarFrame.size.height;
}
if (height < 1) {
height = 47.0; // 刘海 / Island 兜底,避免顶栏塌成「只有 44」
}
return height;
}
用 safeArea 驱动高度,并在 inset 变化时刷新
- (CGFloat)customNavBarHeight {
CGFloat top = self.view.safeAreaInsets.top;
if (top < 1) { top = [self.class statusBarHeight]; }
if (top < 1) { top = 47.0; }
return top + 44.0;
}
- (void)viewSafeAreaInsetsDidChange {
[super viewSafeAreaInsetsDidChange];
[self.navBarHeightConstraint setOffset:[self customNavBarHeight]];
[self.statusBarBgHeightConstraint setOffset:self.view.safeAreaInsets.top > 1
? self.view.safeAreaInsets.top
: [self.class statusBarHeight]];
}
内容区用「顶贴自定义栏底、底贴容器」即可,不要再用 屏幕高 - 错误宏高度 - TabBar 算死高——宏一旦算错,整页会一起错。
若子列表还按「没有导航栏 → 再加 StatusBar+44」做偏移,父级已画自定义顶栏时,记得关掉这层叠加,否则会出现双倍顶距或错位。
四、搜索框「看不见」:先查主题色是不是近白
全页搜索(导航栏 titleView +「取消」)在 iOS 27 上最常见的反馈是:输入框没背景。
排查时先看主题 / 动态色里类似 search_textfield_bg_color 的值。我们遇到的真实配置是 #FEFEFE——在白色导航上等于没画。你在代码里写 fallback #F2F2F7 也没用,因为动态色优先返回主题里的近白。
推荐组合(全页搜索专用)
- 导航栏 / titleView 区域:白底(opaque)
- 输入框:硬编码
#E5E5EA(接近 systemGray5) + 圆角 10 - iOS 26+:先清空系统搜索底图,再设颜色,否则
backgroundColor经常不生效 viewDidLayoutSubviews再刷一次,防止系统盖掉
- (void)applySearchFieldStyle {
UIColor *headerBg = UIColor.whiteColor;
UIColor *fieldBg = [UIColor colorWithRed:0xE5/255.0
green:0xE5/255.0
blue:0xEA/255.0
alpha:1.0];
self.searchBar.backgroundImage = [self imageWithColor:headerBg];
self.searchBar.barTintColor = headerBg;
UITextField *tf = self.searchBar.searchTextField;
tf.borderStyle = UITextBorderStyleNone;
// 关键:先干掉系统默认底,再上自己的色
[self.searchBar setSearchFieldBackgroundImage:[UIImage new]
forState:UIControlStateNormal];
tf.backgroundColor = fieldBg;
tf.layer.backgroundColor = fieldBg.CGColor;
tf.layer.cornerRadius = 10.0;
tf.layer.masksToBounds = YES;
// 可选:36pt 可拉伸圆角底图,兼顾高度观感
UIImage *fill = /* 60×36、圆角 10、颜色 fieldBg,左右 cap 10 */;
[self.searchBar setSearchFieldBackgroundImage:fill
forState:UIControlStateNormal];
// 给 _UISearchBarSearchFieldBackgroundView 上色;Glass 相关子视图可 hidden
[self paintSystemSearchFieldBackground:tf color:fieldBg];
}
另外两个小坑:
- 不要把搜索底视图一律
hidden——以前为了去玻璃这么干,结果只剩图标和 placeholder,更像「没背景」。 - 入口搜索条(灰条点一下再进全页)和全页搜索的逻辑要分开:入口可以跟列表背景;全页在
isEntrance == NO时若backgroundColor = superview.backgroundColor,会再次被刷成导航白,把你刚设的灰底氛围冲掉。
取消按钮建议做成 titleView 里的文字「取消」,而不是完全依赖系统 cancel 样式,减少 Liquid Glass 再挤窄搜索区域的概率。
五、一张「别做 / 要做」对照表
| 别做 | 要做 |
|---|---|
指望 UIDesignRequiresCompatibility | 按真机 Liquid Glass 改 Appearance / 按钮 / 搜索 |
layoutSubviews 改导航栏 frame | 永远不要;会 OOM |
全局 sizeThatFits 压 44 | 高度交给系统;自定义栏用 safeArea |
| 主题近白当搜索底 | 全页搜索硬编码可见灰(如 #E5E5EA) |
| 隐藏整层 SearchFieldBackground | 上色或配合 setSearchFieldBackgroundImage |
启动瞬间 statusBarFrame 算死高 | safeAreaInsets.top + 44,inset 变化再刷 |
六、建议验收路径(发文读者可自测)
- 冷启动进带自定义顶栏的首页:第一行业务 UI 必须完整落在状态栏下方。
- 打开多按钮聊天页:右上角间距应接近旧版,而不是两个大玻璃球。
- 进入全页搜索与搜索结果页:白导航上应能清楚看到灰色圆角输入框 +「取消」。
- 反复进出、旋转(若支持):顶栏高度不回弹成「贴状态栏」。
- 观察启动内存:确认没有导航栏
layoutSubviews改 frame 的「复活」。
写在最后
Liquid Glass 不是「加一层毛玻璃」这么温和。对存量 UIKit 来说,它拆的是默认假设:导航栏高度、按钮排布、搜索底色、甚至你多年没动过的 statusBar + 44 宏。
这轮适配里,最值钱的不是又写一套主题色,而是三条原则:
- 玻璃去不掉时,用 opaque + 合并按钮 + 可见搜索底,换回可读性
- 永远不要用改
UINavigationBar高度去赌旧版像素 - 自定义顶栏只认
safeAreaInsets,不认启动瞬间的 0
如果你也在用 Xcode 27 编老工程,不妨先打开全页搜索和多按钮聊天页——这两处通常最能一眼看出系统已经不再「兼容你的旧审美」。欢迎在评论区说说:你们是系统导航栏为主,还是大量自定义顶栏?两条路径的坑差很多。
延伸阅读
- Apple:UIDesignRequiresCompatibility(构建 iOS 27+ 时忽略)
- Apple:Adopting Liquid Glass
- 同系列可另写:Scene 强制生命周期、提审启动屏、TabBar 浮动玻璃等(本文不展开)