Liquid Glass 正式适配全清单(Xcode 27 目标版)
写在前面
这篇文章是一份多源交叉验证的适配汇总,素材来自 Apple 官方文档与 WWDC session、Apple Developer Forums、iOS 27 SDK 头文件 diff、Stack Overflow、掘金 / 博客园 / fatbobman 等中英文社区,由我逐条比对整理,并借助 AI 辅助做归纳去重。其中「工程实践」一节来自一个大型 UIKit 存量项目真实落地后的结论,这部分你在别处大概搜不到。
数据截止:2026-08-31 环境参照:iOS 26.6.1(2026-08-17)/ Xcode 26.6(17F113,SDK iOS 26.5)/ Xcode 27 beta 6(27A5252f,2026-08-24,SDK iOS 27)/ iOS 27 beta 7(24A5424a)——正式版尚无官宣,Xcode 27 仅支持 Apple Silicon 适用对象:UIKit 为主、需向下兼容 iOS 15+ 的存量 App
阅读建议:如果你只有 5 分钟,看第一节和第六节。第一节可能直接决定你下个版本能不能发出去。
目录
- 一、先说最要紧的:UIScene 不适配会启动崩溃
- 二、Xcode 27 的另两道门槛
- 三、兼容开关的 deadline 已经确定
- 四、一个容易埋雷的地方:怎么判断玻璃开没开
- 五、工程实践:真实项目落地后的 10 条结论
- 六、两处社区误传的纠正
- 七、逐组件适配清单
- 八、iOS 27 SDK 编译后的行为变化与新 API
- 九、测试矩阵:iOS 27 多了一个变量
一、先说最要紧的:UIScene 不适配会启动崩溃
如果你现在的 App 还是「AppDelegate + 手动 UIWindow(frame:) + makeKeyAndVisible」这套旧生命周期,那么用 iOS 27 SDK 编译出来的包,启动就崩。
Apple 论坛的原话:
"When building with the SDK from the next major release after iOS 26, iPadOS 26, macOS 26 and visionOS 26, UIKit will assert that all apps have adopted UIScene life cycle. Apps that fail this assert will crash on launch."
"The scene lifecycle adoption requirement is tied to building against iOS 27. Apps built against previous releases will continue to work as they previously did."
来源:thread/789004、thread/813300、thread/832787
这不是危言耸听,已经有实证:Expo SDK 56 生成的模板工程用 Xcode 27 beta 构建后直接启动失败,原因就是 UIKit 现在要求 scene-based lifecycle(expo/expo#46664)。
两个容易误解的点:
- 判定依据是「你用哪个 SDK 编译」,不是「用户跑哪个系统版本」。 deployment target 写 iOS 15 也一样崩。这和 Liquid Glass 的触发逻辑是同一套机制。
- iOS 26 时代的控制台警告不是提醒你「以后要改」,是最后通知。 从 iOS 26 起 UIKit 就在打 scene lifecycle 的警告了,iOS 27 把 warning 升级成了 assert。
顺带一个连带影响:UIApplicationDelegate.application(_:supportedInterfaceOrientationsFor:) 在 iOS 27 被废弃,替代品是 UIWindowSceneDelegate.supportedInterfaceOrientations(for:)——也就是说,你必须先完成 scene 迁移才能修这个废弃警告,两件事是绑在一起的。
自查三连:
□ Info.plist 或 build setting 里有 UIApplicationSceneManifest 吗?
□ 工程里有 UIWindowSceneDelegate 的实现吗?
□ AppDelegate 里还在手动创建 UIWindow 吗?
先看官方怎么说的(TN3187 原文,比论坛表述更权威):
"In the next major release following iOS 26, UIScene lifecycle will be required when building with the latest SDK; otherwise, your app won't launch."
"While supporting multiple scenes is encouraged, only adoption of scene life-cycle is required."
第二句很重要,能省掉一大半工作量恐慌:你只需要适配 scene 生命周期,不强制支持多窗口。 多场景(iPad 多开、Stage Manager)是「鼓励」而非「要求」,UIApplicationSupportsMultipleScenes 保持 false 完全合规。
Apple 给的「是否需要迁移」判据只有两条,满足任一条就是需要迁:
Info.plist里缺UIApplicationSceneManifest,或它没有任何 configuration- 没有实现
application(_:configurationForConnecting:options:)
日志的演进也说明了这件事推了多久:iOS 18.4 起就在打 This process does not adopt UIScene lifecycle. This will become an assert in a future version.,iOS 26 改成 UIScene lifecycle will soon be required. Failure to adopt will result in an assert in the future.,下一个大版本变成硬性要求。
最小实现就是把创建 window 与设置 rootViewController 从 didFinishLaunching 搬到 scene(_:willConnectTo:options:):
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = YourRootViewController()
window?.makeKeyAndVisible()
}
}
生命周期方法的对应关系(官方映射表):
| UIApplicationDelegate | UISceneDelegate |
|---|---|
applicationDidBecomeActive(_:) | sceneDidBecomeActive(_:) |
applicationWillResignActive(_:) | sceneWillResignActive(_:) |
applicationDidEnterBackground(_:) | sceneDidEnterBackground(_:) |
applicationWillEnterForeground(_:) | sceneWillEnterForeground(_:) |
注意 Info.plist 里 UISceneDelegateClassName 填 Swift 类时要带模块前缀($(PRODUCT_MODULE_NAME).SceneDelegate),这是黑屏的常见成因之一。纯代码构建 UI 的工程不要留 UISceneStoryboardFile。
比启动崩溃更该防的是「深链静默失效」。
启动崩溃在测试期必然暴露,反而不可怕。真正会漏到线上的是这个:一旦 Info.plist 声明了 UIApplicationSceneManifest,系统就静默停止向 AppDelegate 投递 URL 事件——没有 crash、没有警告、控制台也不报错,只是你的 application:openURL:options: 再也不会被调用。表现是推送点击、Universal Link、自定义 scheme 全部失效,而 App 看起来完全正常。
三条 URL 回调必须全部迁到 SceneDelegate:
| 旧(AppDelegate) | 新(UIWindowSceneDelegate) |
|---|---|
application:openURL:options: | scene:openURLContexts: |
application:continueUserActivity:restorationHandler: | scene:continueUserActivity: |
冷启动通过 launchOptions 取 URL | scene:willConnectToSession:options: 的 connectionOptions |
还有两个时序陷阱:
- 冷启动时 URL 在
willConnectTo就到达,早于业务层初始化完成。此时直接分发会因为路由表/账号态还没就绪而丢失。正确做法是在willConnectTo里先把 URL 暂存(stash),等业务初始化完成后再消费。 - 冷启动可能同时触发
connectionOptions与openURLContexts,需要去重,否则同一个深链会被处理两次(官方论坛有专门 PSA)。
来源:Apple Developer Forums thread/801287(openURLContexts 冷启动双触发)、TN3187
所以 UIScene 迁移的验收标准不是「能启动」,而是「深链、推送跳转、Universal Link 全链路可用」。 把这三项写进迁移的 Definition of Done,不要留到回归测试阶段才发现。
官方迁移指南:TN3187: Migrating to the UIKit scene-based life cycle
所以适配 Liquid Glass 的正确排期是:先过 UIScene,再谈玻璃。 视觉做得再漂亮,App 起不来都是零。
二、Xcode 27 的另两道门槛
2.1 启动屏必须有,否则被拒绝
用 27.0 SDK 构建的 iOS / iPadOS App,Info.plist 必须含 UILaunchStoryboardName、UILaunchStoryboards、UILaunchScreen、UILaunchScreens 四者之一。缺了的话,App Store Connect 开始接收 27.0 SDK 构建之后会直接拒收。
准确说,启动屏本身是长期既有的要求,iOS 27 SDK 做的是把它升级成硬性拒收条件,不是新发明了一条规则。绝大多数正常工程本来就满足,属于顺手核查项。
一个容易自己骗自己的坑:现代 Xcode 工程通常用 build setting INFOPLIST_KEY_UILaunchStoryboardName 注入这个 key,构建时才写进 Info.plist。你直接搜 Info.plist 源文件会搜不到,很容易误判成缺失然后白改一通。检查时要连 project.pbxproj 一起搜。
2.2 App Store 已经强制 iOS 26 SDK
这条是既成事实了:2026 年 4 月 28 日起,上传 App Store Connect 的包必须用 Xcode 26+ / iOS 26 SDK 构建,没有宽限期,没有延期。
也就是说,你的 App 只要在 4 月 28 日之后提审过一次,它已经是 iOS 26 SDK 的包,Liquid Glass 已经在你用户手机上生效了——除非你用了下面那个开关。
三、兼容开关的 deadline 已经确定
UIDesignRequiresCompatibility 这个 Info.plist 开关,曾经是所有人的救命稻草:
<key>UIDesignRequiresCompatibility</key>
<true/>
现在它的结局已经写死了。Apple 论坛明确回复:
"When you build your app with the Xcode 27 SDKs, the value of
UIDesignRequiresCompatibilitywill be ignored, even if your app's deployment target is set to the 26 releases or earlier."
来源:thread/838637、thread/832710。更早还有一句 "We intend this option to be removed in the next major release."(thread/832543)
判定逻辑还是看链接的 SDK:iOS 27 SDK 编译 → 强制玻璃;老 SDK 编译 → 在 iOS 27 上仍是旧设计。
Xcode 27 已经在 2026 年 6 月 8 日(WWDC26)进 beta,截至本文数据截止日(2026-08-31)跑到 beta 6(iOS 27 到 beta 7),正式版尚无官宣、也还没有 RC。另外 App Store Connect 目前还没开放 27.0 SDK 上架,Apple 也没公布 iOS 27 SDK 的强制时间表——也就是说这道门槛现在只在你本地或 CI 用 Xcode 27 编译时才会撞上。
结论:如果使用 Xcode 27 打包的话,那应该就必须要适配了。已经加了的建议主动删掉——越早暴露真实效果,越早发现问题。如果不用 Xcode 27 打包, 预计能延到27年春季。
四、一个容易埋雷的地方:怎么判断玻璃开没开
做渐进式适配时,很多团队会封装一个「当前是否启用了液态玻璃」的运行时判断,让自定义控件决定走玻璃分支还是旧样式分支。最直觉的实现是读 Info.plist:
// ⚠️ 这个思路在 Xcode 27 下会出错
BOOL isLiquidGlassEnabled(void) {
if (@available(iOS 26.0, *)) {
id value = NSBundle.mainBundle.infoDictionary[@"UIDesignRequiresCompatibility"];
if (value == nil) { return YES; }
return ![value boolValue];
}
return NO;
}
这个实现在 Xcode 26 时代是对的。但 Xcode 27 SDK 编译后,系统会忽略这个 key 照样开玻璃,而你的代码还在读它。一旦这个 key 因为任何原因存在——有人为别的目的加回来、某个 App Extension 或 target 的 plist 里有它、CI 模板注入——判断就整体反了:
- 系统组件:玻璃外观(系统忽略 key)
- 你的自定义控件:走 no-op 退回旧样式(代码以为玻璃没开)
结果是视觉割裂,而且只在特定构建配置下出现,排查起来非常痛苦。
修法:加 SDK 版本保护,把「iOS 27+ SDK 编译时该 key 无效」这个事实钉进代码:
BOOL isLiquidGlassEnabled(void) {
if (@available(iOS 26.0, *)) {
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 270000
// 用 iOS 27+ SDK 编译:系统忽略 UIDesignRequiresCompatibility,玻璃恒定开启
return YES;
#else
id value = NSBundle.mainBundle.infoDictionary[@"UIDesignRequiresCompatibility"];
if (value == nil) { return YES; }
if ([value isKindOfClass:[NSNumber class]]) { return ![value boolValue]; }
if ([value isKindOfClass:[NSString class]]) {
NSString *s = [(NSString *)value lowercaseString];
return !([s isEqualToString:@"yes"] || [s isEqualToString:@"true"] || [s isEqualToString:@"1"]);
}
return YES;
#endif
}
return NO;
}
另外提醒一句:这个 key 的值可能是 NSNumber 也可能是 NSString(手写 plist 或某些工具注入时常见),只写 boolValue 会漏。
五、工程实践:真实项目落地后的 10 条结论
这一节来自一个 UIKit 存量项目的实际改造(导航栏 + TabBar 全量适配),是踩过之后的结论,不是文档推导。
5.1 Configuration.glass() 和 UIVisualEffectView(UIGlassEffect) 不是一回事
这是最容易选错的一步:
UIButton.Configuration.glass() | UIVisualEffectView(UIGlassEffect, isInteractive: true) | |
|---|---|---|
| 玻璃归属 | 按钮自身的背景层,铺满 bounds | 独立玻璃块,按钮放进它的 contentView |
| 按压反馈 | 只有亮度变化 | 玻璃本体被系统拉动形变 |
| 依赖 | UIButton.Configuration 体系 + configuration.image | 无,图标仍由按钮原有 setImage 提供 |
那批导航栏图标按钮最初全用前者,后来全部改成了后者。原因两条:一是按压没有玻璃形变,观感不对,玻璃感全靠静态背景撑;二是 Configuration 体系实测在非调试启动下表现与 Debug 不一致。
选择建议:要真实玻璃交互质感的按钮走 UIVisualEffectView(UIGlassEffect, isInteractive: true);只要个静态玻璃底的,Configuration.glass() 更省事。
5.2 玻璃不能嵌套
玻璃块内部承载图标和点击的按钮,必须是没有玻璃配置的普通按钮。外层已经给了玻璃背景,里面再套 Configuration.glass() 就是两层玻璃叠加,视觉上很脏。
5.3 容器不要设 cornerRadius + clipsToBounds
UIGlassContainerEffect 容器只负责合并渲染子玻璃元素,它本身不是一块可见玻璃。给它加圆角裁剪没有任何视觉收益,反而会把元素融合时溢出的流体形变裁掉——按压和出现动画会被切边。
胶囊形状应该由每个玻璃块的 cornerConfiguration = .capsule()(iOS 26 起 UIView 提供)决定:块宽 > 高时是胶囊,等宽高(比如 44×44)时就是正圆。
5.4 容器融合出来的不是一条完整胶囊
这个很反直觉:UIGlassContainerEffect 把多块玻璃融合,得到的是**「多个正圆 + 融合桥」**,不是一条连续胶囊。
如果你想要的是「一条胶囊里并排放两个按钮」,正确做法是创建单个 UIGlassEffect 块,把按钮用 UIStackView 放进它的 contentView,宽度由内容撑出。用容器只会得到两个圆连着一根桥。
5.5 spacing 是融合阈值,要设得比实际间距大
UIGlassContainerEffect.spacing 不是「元素间距」,是「开始融合」的距离阈值。它必须大于实际元素间距,否则两块玻璃根本不会融合,还是分开的两个圆。
5.6 子视图必须加到 contentView
直接 addSubview 到 UIVisualEffectView 是未定义行为。加到 contentView 上,按钮照常收到点击。UIVisualEffectView 默认 isUserInteractionEnabled = true,不要去关它。
5.7 套玻璃要在约束建立之后
玻璃背景按自身 bounds 铺满,尺寸还没定的时候套配置,首帧会出现一个非正圆的怪胶囊。实践是在 setupViews() / layout() 建完约束之后紧跟一行调用。
5.8 一定要留一个全局回退开关
这条是血泪经验。玻璃改造必然伴随设计走查反复,给玻璃入口加一个全局布尔开关,true 时所有接入点退化为 no-op:
extension UIButton {
/// 置为 true 时所有玻璃接入点退化为 no-op
static var needsLiquidGlassFallback: Bool { false }
static var isLiquidGlassStyleEnabled: Bool {
guard !needsLiquidGlassFallback else { return false }
if #available(iOS 26.0, *) { return isLiquidGlassEnabled() }
return false
}
}
走查不达标时改一处就能整体回退,不用逐个文件删代码。这点投入在评审阶段回报极高。
5.9 图标被二次染色的坑
如果你的主题图片本身已经着好色了(比如图片名里就带颜色 token),要显式声明 .alwaysOriginal。否则在 Configuration 体系下会被 baseForegroundColor 或系统 tint 二次染色,图标变色甚至丢色。
也不要改用 config.baseForegroundColor——颜色语义已经在图片里了,再引一个前景色 token 就有两个颜色来源,后面没人搞得清该改哪个。
5.10 走 Configuration 路线,主题切换要自己刷
多数主题框架的自动刷新只处理 setImage(_:for:),碰不到 configuration。用 Configuration 路线时,主题切换回调里必须自己重新取图写回 configuration?.image,否则切主题后图标不变。
六、两处社区误传的纠正
整理资料时发现这两条在中文社区传播很广,但和官方文档对不上。
6.1 「topEdgeEffect.style = .none 关掉边缘效果」——这个 API 不存在
这个写法出现频率非常高,但 UIScrollEdgeEffect.Style 的取值只有 .automatic / .soft / .hard,没有 .none。
禁用要用独立的 isHidden 属性。Apple 文档给的签名是 var isHidden: Bool { get set },默认 false:
if #available(iOS 26.0, *) {
// ❌ 社区常见写法,Style 里没有 .none
// scrollView.topEdgeEffect.style = .none
// ✅ 禁用
scrollView.topEdgeEffect.isHidden = true
scrollView.bottomEdgeEffect.isHidden = true
webView.scrollView.topEdgeEffect.isHidden = true
// ✅ 换风格而非禁用
scrollView.topEdgeEffect.style = .soft
}
SwiftUI 侧同理:scrollEdgeEffectStyle(_:for:) 只能换风格,要彻底关掉用 scrollEdgeEffectDisabled()。
我推测这个错误的来源是跨端框架的封装层——React Native Navigation 的配置项确实叫 hidden: true,被转述成 Swift 时变成了想象中的 .none。
来源:UIScrollEdgeEffect.isHidden、SO 79673815、Hacking with Swift
⚠️ 另外这套 API 在某些视图层级下会静默失效:Stack Overflow 有 topEdgeEffect 设了完全没反应的案例,react-native-screens 也报告过 scroll view 不在首个 subview 链上时配置被忽略。设了没用的话,先确认你拿到的是真正承载滚动的那个 scrollView。(SO 79825968、react-native-screens#4369)
6.2 「iOS 26 的 rightBarButtonItems 顺序反转了」——大概率是误判
不少文章说 iOS 26 里 rightBarButtonItems 数组第一个元素变成显示在最右边,与低版本相反,需要按系统版本反转数组。
但翻 Apple 的历史文档,这一直就是 UIKit 的既有行为:
"Items are displayed right-to-left in the same order as they appear in the array. Thus, the first item in the array is the rightmost item and other items are added to the left of the previous item."
Hacking with Swift 在 2019 年就写过同一件事:leftBarButtonItems 按数组顺序排,rightBarButtonItems 反过来,两者都是「从外向内填充」。
Apple 官方文档和 release notes 里找不到这项变更记录。更可能的解释是:那些项目原先用的是自定义导航栏封装,iOS 26 改了内部布局后暴露了封装里的假设,而不是 UIKit 语义变了。
结论:别照抄「按版本反转数组」的代码——如果你的项目本来是对的,加了分支反而错。先在 iOS 18 和 iOS 26 上各跑一次确认现象。
更稳的做法是用语义化 API,它自己处理排列和 RTL:
if #available(iOS 16.0, *) {
navigationItem.trailingItemGroups = [
UIBarButtonItemGroup(barButtonItems: [item1, item2], representativeItem: nil)
]
navigationItem.leadingItemGroups = [
UIBarButtonItemGroup(barButtonItems: [backItem], representativeItem: nil)
]
}
七、逐组件适配清单
7.1 导航栏按钮多出一层玻璃底
if #available(iOS 26.0, *) {
barButtonItem.hidesSharedBackground = true
}
hidesSharedBackground 是 iOS 26 正式 API(WWDC25-284),不是私有 API。
存量项目里 UIBarButtonItem 创建点动辄几百处,逐个改不现实。先定设计口径:接受系统玻璃底,还是全部统一到自建玻璃。混编项目可以用 ObjC 分类 + swizzling 统一兜底:
@implementation UIBarButtonItem (DefaultHideBackground)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL selectors[] = {
@selector(initWithTitle:style:target:action:),
@selector(initWithImage:style:target:action:),
@selector(initWithBarButtonSystemItem:target:action:),
@selector(initWithCustomView:),
@selector(initWithCoder:),
@selector(init)
};
for (NSUInteger i = 0; i < sizeof(selectors) / sizeof(SEL); i++) {
SEL original = selectors[i];
SEL swizzled = NSSelectorFromString(
[@"lg_" stringByAppendingString:NSStringFromSelector(original)]);
Method originalMethod = class_getInstanceMethod(self, original);
Method swizzledMethod = class_getInstanceMethod(self, swizzled);
if (originalMethod && swizzledMethod) {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
});
}
- (void)lg_applyDefaultHidesSharedBackground {
if (@available(iOS 26.0, *)) {
self.hidesSharedBackground = YES;
}
}
// ⚠️ selectors[] 里的每个入口都必须有对应的 lg_ 实现,6 个一个都不能少
- (instancetype)lg_initWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
self = [self lg_initWithTitle:title style:style target:target action:action];
[self lg_applyDefaultHidesSharedBackground];
return self;
}
- (instancetype)lg_initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
self = [self lg_initWithImage:image style:style target:target action:action];
[self lg_applyDefaultHidesSharedBackground];
return self;
}
- (instancetype)lg_initWithBarButtonSystemItem:(UIBarButtonSystemItem)item target:(id)target action:(SEL)action {
self = [self lg_initWithBarButtonSystemItem:item target:target action:action];
[self lg_applyDefaultHidesSharedBackground];
return self;
}
- (instancetype)lg_initWithCustomView:(UIView *)customView {
self = [self lg_initWithCustomView:customView];
[self lg_applyDefaultHidesSharedBackground];
return self;
}
- (instancetype)lg_initWithCoder:(NSCoder *)coder {
self = [self lg_initWithCoder:coder];
[self lg_applyDefaultHidesSharedBackground];
return self;
}
- (instancetype)lg_init {
self = [self lg_init];
[self lg_applyDefaultHidesSharedBackground];
return self;
}
@end
⚠️ 这里有个很容易踩的坑:
selectors[]列了 6 个入口,但交换只在originalMethod && swizzledMethod都非空时才发生——没写对应lg_实现的 selector 会被静默跳过,不报错也不生效。如果你只补了lg_initWithCustomView:就上线,结果是「以为 6 路都兜底了,实际只有 customView 一路生效」,而且没有任何编译或运行时提示。要么把 6 个都实现,要么把selectors[]砍到只留你真正实现的那几个。
这段实现的思路参考了 掘金 Andy_GF《iOS26 适配之 UIBarButtonItem》,我在此基础上补齐了全部 init 入口、加了
dispatch_once保护并显式标注了静默跳过问题。
纯 Swift 工程不建议 swizzling,走基类统一设置更可控。
全局兜底思路参考:掘金 Andy_GF
顺带一提,UIBarButtonItem.Style.done 在 iOS 26 已 deprecated。
7.2 tintColor 全局设置失效
UINavigationBar.appearance().tintColor 和 window.tintColor 都不再生效,按钮全变系统蓝。Apple 在论坛确认这是预期行为,不是 bug(thread/788176)。只能逐 item 设置,建议在基类 VC 里统一处理。
7.3 塞进 navigationBar 的自定义视图消失
从二级页面返回后,之前 addSubview: 到 navigationBar 上的自定义视图不见了。原因是 iOS 26 导航栏换了 compositing layer 结构,非官方子视图会在 push / pop 动画期间被系统内部层盖住。
// ❌ 不再可靠
[self.navigationController.navigationBar addSubview:customNaviView];
// ✅ 挂到 navigationController.view
[self.navigationController.view addSubview:customNaviView];
// 记得在二级页面 viewWillAppear 里隐藏
另外 UIBarButtonItem(customView:) 传入的固定 frame 也不再被尊重,必须给宽高约束。
7.4 TabBar
KVC 替换 tabBar —— 关于这条我想把话说准。社区常见说法是「iOS 26 直接 crash」,但来源多是单人实测博客,不是 Apple 官方结论。我见过长期保留 setValue(customTabBar, forKey: "tabBar") 且在 iOS 26 上正常跑的项目。
准确的表述是:这是依赖私有实现结构的高危写法。建议不按「必崩」排期,但列入 iOS 27 beta 专项回归清单——大版本更替是 UIKit 内部结构最容易变的时候。
要改的话用 addSubview:
tabBar.subviews.forEach { $0.removeFromSuperview() }
customTabBar.frame = tabBar.bounds
tabBar.addSubview(customTabBar)
改完验证两件事:中间凸起按钮的点击是否被系统拦截(可能要重写 hitTest(_:with:))、系统 Glass 选中胶囊是否和自定义 UI 叠一起。
isTranslucent:[UITabBar appearance].translucent = NO 这类全局设定要在 iOS 26 上条件排除或直接移除。
已知系统 Bug(截至发文未见官方修复确认):
- TabBar 动画导致 App 挂起,Instruments 指向 UIKitCore(809465)
hidesBottomBarWhenPushed = true时首个 push 动画异常(809603)- Tab 图标切换时闪烁(SO 79753928)
- 内容不延伸到浮动 TabBar 之下(SO 79747677)
7.5 Scroll Edge Effect
写法见第六节(那是社区错得最多的地方)。典型受害场景:WKWebView 顶部内容被糊、图表 / 自绘视图边缘被干扰、UIPageViewController 内部误触发、键盘弹起后 tableView 底部出现 alpha mask。
四个边独立:topEdgeEffect / bottomEdgeEffect / leadingEdgeEffect / trailingEdgeEffect。注意是 leading / trailing 而不是 left / right,RTL 会自动镜像。
7.6 ActionSheet
iOS 26 的 iPhone 上,系统 ActionSheet 变成锚定式 popover(和 iPad 统一),没设 sourceView 会异常居中甚至崩:
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alert.popoverPresentationController?.sourceView = sender
alert.popoverPresentationController?.sourceRect = sender.bounds
present(alert, animated: true)
不过先搜一下再决定投入:如果你的业务弹窗走的是自研 View 层组件而不是系统 UIAlertController,这一项可能整体不适用。我核查过的那个项目全库的 UIAlertController 屈指可数且全是 .alert,一处系统 actionSheet 都没有,这一项直接跳过了。
7.7 其他
- 搜索栏:默认位置移到了底部,
preferredSearchBarPlacement = .stacked恢复顶部;iOS 26 另有.integratedCentered - Sheet:自动带 Glass 背景,原来的
presentationBackground自定义可能冲突,建议交还系统;Popover 圆角变大要加 padding 防裁切 - Toolbar:按钮自动分组、间距变化、过多时溢出到「…」菜单;
inputAccessoryView同样会玻璃化 - Glass 按钮:
UIButton.Configuration.glass() / .clearGlass() / .prominentGlass() / .prominentClearGlass()
八、iOS 27 SDK 编译后的行为变化与新 API
这一节素材主要来自 What's New in UIKit in iOS 27(作者 diff 了 iOS 26.2 SDK 与 iOS 27 SDK 的 UIKit 头文件,信息密度很高)+ iOS 27 Release Notes。
8.1 自动生效的行为变化(需回归)
- Presented VC 的 trait 继承链变了:UIKit 现在沿被 present 的 VC 的
superview链向上穿过中间 presentation views 取 trait,不再直接跳到 presentation controller。自定义UIPresentationController子类、或依赖 trait 直接来自 presentation controller 的被 present 控制器,可能需要显式 trait 传播。自研弹窗和半屏 sheet 要逐个回归深色模式与字号继承。 - 居中搜索栏的 scope bar 变成 inline:和搜索框同一行;搜索框在导航栏里时 scope bar 也在导航栏里并排。
- Menu 图片默认隐藏(iPadOS 27 / macOS 27):需
UIMenuElement.preferredImageVisibility显式 opt-in。系统通用项(设置、分享、打印)仍有默认图。 - Siri / Apple Intelligence 可能在没有用户拖拽手势时调用
UIDragInteractionDelegate:如果你在dragInteraction(_:sessionWillBegin:)里做动画或弹模态 UI,挪到dragInteraction(_:sessionDidMove:)。 - 外接显示器非交互场景不再自动提供:改用
UISceneAccessory.externalNonInteractive(...)+UIViewController.registerSceneAccessory(_:)注册。
8.2 废弃
| API | 替代 |
|---|---|
UIApplicationDelegate.application(_:supportedInterfaceOrientationsFor:) | UIWindowSceneDelegate.supportedInterfaceOrientations(for:) |
UIScreen.displayLink(target:selector:) / UIApplication 等价物 | UIWindowScene.displayLink(target:selector:) |
UIAccelerometer / UIAcceleration / UIAccelerationValue | CoreMotion(从 deprecated 升为 obsoleted) |
注意区分:项目里常用的 CADisplayLink 本身没被废弃,被废弃的是 UIScreen / UIApplication 上那两个 displayLink 工厂入口。iOS 27 没有移除任何 public UIKit 类。
8.3 新增的 Liquid Glass 相关 API
// 导航栏 / 工具栏最小化
if #available(iOS 27.0, *) {
navigationItem.barMinimizeBehavior = .onScrollDown // UIBarMinimizeBehavior
// navigationItem.barMinimizationSafeAreaAdjustment = ...
}
// 导航栏按钮空间优先级 / 去内边距
if #available(iOS 27.0, *) {
saveButton.visibilityPriority = UIBarButtonItemVisibilityPriority(higherThan: shareButton.visibilityPriority)
// 另有 paddingRemoved
}
// TabBar
if #available(iOS 27.0, *) {
tabBarController.performBatchUpdates { /* 多个 tab 变更一起动画 */ }
tabBarController.setProminentTabIdentifier(id, animated: true)
}
UIBarButtonItem.paddingRemoved 值得优先关注——玻璃容器带来的导航栏按钮间距和热区问题,之前只能手工塞 fixedSpace spacer,现在有正式 API 了。
顺便提三个容易漏掉的 iOS 26 点版本 API:UITab.selectedImage(26.1)、UITabGroup.collapsedByDefault(26.1)、UISearchTab.identifier(26.4)。
九、测试矩阵:iOS 27 多了一个变量
iOS 27 用户侧新增了 Liquid Glass 透明度滑块,能从 ultra clear 无级调节到 fully tinted,比 iOS 26 那个二元的「降低透明度」开关走得远得多(MacRumors)。
这直接改变测试口径:iOS 26 只需覆盖开关的开 / 关两态,iOS 27 要覆盖滑块两端极值 + 中间态。玻璃上的文字和图标对比度在极端值下最容易翻车。
两点要说清楚:① 这是用户系统设置项(Settings → Appearance → Liquid Glass),目前没有对应的公开开发者 API——iOS 27 的 UIKit 头文件 diff 里查不到任何新增入口,你能编程读到的仍然只有
UIAccessibility.isReduceTransparencyEnabled。所以不要试图按滑块档位做分支,只能保证在各档位下都可读。② 社区流传的「0–100 intensity」之类说法没有官方背书。
⚠️ 另有一条 iOS 27 beta 的回归需要盯:有开发者报告 iOS 27 beta 4 起,同一个二进制的原生 Liquid Glass tab bar 比 iOS 26.5 明显更不透明(偏磨砂灰,且未加任何自定义 blur),方向上与透明度整体调淡一致。如果你已经接入了玻璃,升到 iOS 27 beta 后需要重新走查观感。来源:Apple Developer Forums thread/840652(社区实证,未见官方确认)
其余清单:
启动与生命周期(Xcode 27 重点):冷启动 / 热启动 / 后台恢复、多任务切换与分屏、推送点击唤起、URL Scheme 与 Universal Link 唤起、状态恢复。
功能:各页面导航栏按钮位置 / 颜色 / 可点击性;TabBar 切换无卡死;ActionSheet 锚定;搜索栏位置;Sheet / Popover 内容未被裁切;WebView 无异常模糊;图表可读性。
多语言:LTR / RTL 全流程、运行时切语言后 Glass 是否正确刷新、长文案语言下按钮是否被玻璃容器挤截断。
辅助功能:Light / Dark、「降低透明度」、「增加对比度」、iOS 27 透明度滑块。
设备:小屏(SE 系列)、主流、Pro Max、iPad(如适用)、最低支持版本设备确认未受影响、iOS 26 真机、iOS 27 beta 真机。
「降低透明度」和「增加对比度」这两个辅助功能开关要特别强调——Liquid Glass 在这两种模式下走的是完全不同的渲染路径,最容易漏测,上线后又最容易被投诉。
附:确认可用的 API 清单
iOS 26 · UIKit:UIGlassEffect、UIGlassContainerEffect、UIBackgroundExtensionView、UIScrollEdgeElementContainerInteraction、UIButton.Configuration.glass() 四个变体、UIBarButtonItem.hidesSharedBackground、UITabBarController.tabBarMinimizeBehavior、UITabBarController.bottomAccessory、UIScrollView.top/bottom/leading/trailingEdgeEffect、UIScrollEdgeEffect.isHidden、UIScrollEdgeEffect.style、UIView.cornerConfiguration
iOS 26 · SwiftUI:.glassEffect(_:in:)、GlassEffectContainer、.glassEffectID(_:in:)、.scrollEdgeEffectStyle(_:for:)、.scrollEdgeEffectDisabled()、.buttonStyle(.glass)、.buttonStyle(.glassProminent)、.tabBarMinimizeBehavior(_:)
iOS 27 · UIKit:UINavigationItem.barMinimizeBehavior、UIBarMinimizeBehavior、UIBarMinimizationSafeAreaAdjustment、UIBarButtonItem.visibilityPriority、UIBarButtonItemVisibilityPriority、UIBarButtonItem.paddingRemoved、UITabBarController.performBatchUpdates(_:)、UITabBarController.prominentTabIdentifier、UISceneAccessory、UISceneClosureConfirmation、UIWindowScene.displayLink(target:selector:)
不要用:topEdgeEffect.style = .none、UIGlassEffect 带参构造器、.glassBackgroundEffect(in:displayMode:)、任何声称的官方 back-port 框架(Apple 论坛已明确无 back-port)
主要参考
Apple 官方
- Adopting Liquid Glass
- WWDC25-284 Build a UIKit app with the new design / WWDC25-323 SwiftUI 版
- TN3187: Migrating to the UIKit scene-based life cycle
- iOS & iPadOS 27 Release Notes
- UIScrollEdgeEffect.isHidden
- Forums:789004 / 813300 / 832787(UIScene 强制)、838637 / 832710 / 832543(兼容开关)、788176(tintColor)、809465 / 809603(TabBar Bug)
- Upcoming Requirements / SDK 最低要求公告
社区
- Kyle Howells - What's New in UIKit in iOS 27(SDK 头文件 diff)
- fatbobman:UIKit Compatibility Pitfalls / Grow on iOS 26
- 掘金 Andy_GF - UIBarButtonItem 全局处理、YungFan - UITabBarController
- Donny Wals - Opting out of Liquid Glass
- Hacking with Swift - scroll edge effect
- expo/expo#46664(UIScene 强制的实证)
- MacRumors - iOS 27 的 Liquid Glass 变化
免责说明:文中代码示例来自官方 session、文档与真实项目落地整理,未在我本地逐条编译验证。如果你发现文中有错,欢迎评论区指出,我会更新。
AI标签:该文档部分内容使用AI生成。