Liquid Glass:UIKit 适配踩坑实录

尽管 Liquid Glass 已经推出一年,但它带来的兼容性问题并未完全消失。SLIT_STUDIO 的开发者 ⁠Megabits 结合真实项目,总结了 UIKit 在适配过程中的多个典型坑点及应对方法。

这文章拖了很久,直到现在 iOS 27 都出了,我才来写文章讲 iOS 26。这主要的原因就是 iOS 26 上很多 Bug 非常诡异,很多时候我真的不确定这是不是系统的问题,更不确定我是不是真的修好了。不过虽然来晚了,我想依然会有人需要这篇文章,毕竟即便有了 iOS 27,我们也不可能现在就把 iOS 26 丢掉。所以就来看看苹果这次又给我们挖了哪些坑吧。

UIBarButtonItem

如果你的 App 是用 UIKit 而不是 SwiftUI 做的,而且你还用 customView 写了 UIBarButtonItem,那你有福了。我这里写了一个简单的例子来看看会发生什么。

这是一个 iOS 18 模拟器中运行的 Demo。下面的代码我会做很多省略,只留下重要的部分。

Swift
// 传入尺寸
override init(frame: CGRect) {
    imageFrame = frame
    super.init(frame: frame)
    setup()
}
    
func setup() {
    // (此处省略)
    translatesAutoresizingMaskIntoConstraints = false
    imageView.translatesAutoresizingMaskIntoConstraints = false
    imageView.contentMode = .scaleAspectFit
    addSubview(imageView)
    NSLayoutConstraint.activate([
        imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
        imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
        imageView.topAnchor.constraint(equalTo: topAnchor),
        imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
        imageView.widthAnchor.constraint(equalToConstant: imageFrame.width),
        imageView.heightAnchor.constraint(equalToConstant: imageFrame.height),
    ])
    // (此处省略)
}

似乎看起来都还 OK,不过在 iOS 26 中就大变样了。

我们可以发现两个问题,首先图片成了长方形,其次颜色也都没有了。这得一个一个解决。

尺寸问题

我想注意力惊人的你一定注意到了,似乎在 init 时传入的尺寸被系统给动过了,不是正方形了。加两行试试看呢?

Swift
widthAnchor.constraint(equalToConstant: imageFrame.width),
heightAnchor.constraint(equalToConstant: imageFrame.height),

似乎没什么效果。

原来,还要让图片完全不依赖外部宽高布局才行。

Swift
NSLayoutConstraint.activate([
    widthAnchor.constraint(equalToConstant: imageFrame.width),
    heightAnchor.constraint(equalToConstant: imageFrame.height),
    imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
    imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
    imageView.widthAnchor.constraint(equalToConstant: imageFrame.width),
    imageView.heightAnchor.constraint(equalToConstant: imageFrame.height),
])

这样看起来对劲多了。

这两个缺一不可,否则甚至可能会出现更离谱的效果。

颜色问题

下一个要解决的是颜色问题,我这里有颜色的部分都是用 UILayer 来做的,可以看到全被整没了。但是,图片中的颜色则不会消失。证明这一点很简单,把红点换成一张图即可。

我最后也没有搞清楚为什么会有这种行为,也没有找到其他的什么好办法,但这却让我找到了彻底的解决方案。那就是,用 SwiftUI。。。

彻底的解法

只要将 customView 改成是用 SwiftUI 画的,拿 UIHostingController 套一个,一切的问题就都解决了,布局也对了。你就说离谱不各位。我到现在都不明白这是 Bug 还是特性。

进而我们还可以把角标改成用新的 API 画到外面,这个按钮就基本上完美了。

Swift
item.badge = .string("3")
item.badge?.backgroundColor = .systemRed
item.badge?.foregroundColor = .white

还没完呢

你以为这就完了?一切才刚刚开始。iOS 26 开始提供的这一套角标 API 有一个非常致命的问题,就是内容可能会不更新。具体什么时候会发生我已经懒得追究了,总之最简单的解决办法就是在改了角标之后把 UIBarButtonItem 的内容动一下,比如像下面这样,拿下去再拿上来,立竿见影。

Swift
item.badge = .string("3")
item.customView = nil
item.customView = customView

还有一个问题是,如果你按钮是用 rightBarButtonItems 设置的,你就可能会遇到按钮顺序反过来的情况,这似乎是和加载的时间点有关的,因为 DispatchQueue.main.async 一下我这里就能解决,不过还是推荐大家用新的 trailingItemGroups。

Swift
self.navigationItem.trailingItemGroups = [
    UIBarButtonItemGroup(barButtonItems: [rightBarButtonItems1], representativeItem: nil),
    UIBarButtonItemGroup(barButtonItems: [rightBarButtonItems2], representativeItem: nil)
]

不跟上 HIG 的代价

目前我手上这个 App 会在 NavigationBar 上设置背景色。这个行为现在已经是不推荐的了。如果你非要设置,就会出现一个坑就是 UIBarButtonItem 并不是根据 NavigationBar 的背景色,而是根据更底层 ScrollView 中的内容变深浅模式的。但是!颜色却是跟着这个背景色的。这就导致浅色的时候看起来还好:

深色的时候就非常恐怖:

我找到的看起来效果还行的方案就是给整个按钮固定成白色。

Swift
scanItem.style = .prominent
scanItem.tintColor = .white

此外在这种情况下还有一个很细节的问题,如果你的图标不是黑色的,你用 UIKit 的 CustomView 和 SwiftUI 的 CustomView 会渲染出不一样的颜色。UIKit 的 CustomView 看起来要黑一些,受底部背景色的影响,和不用 CustomView 的时候效果是一致的。SwiftUI 的则并不受底部背景色的影响,接近原本的颜色。

UITabBarController

TabBar 自己的问题

我们来看下面这样一个美丽的 TabBar。

假设你的 App 中有一个用于登陆的 sheet,在用户登录后,你需要重写 TabBar 的内容。于是你在用户登录之后删除了并重新放置了 UITabBarController。

Swift
let tb = UITabBarController()
tabBarVC?.willMove(toParent: nil)
tabBarVC?.view.removeFromSuperview()
tabBarVC?.removeFromParent()
tb.viewControllers = [
    makeTab(title: "Home", systemImage: "house.fill"),
    makeTab(title: "Search", systemImage: "magnifyingglass"),
    makeTab(title: "Profile", systemImage: "person.crop.circle.fill"),
]

addChild(tb)
tb.view.frame = view.bounds
tb.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(tb.view)
tb.didMove(toParent: self)

self.dismiss(animated: true)

那么恭喜你你碰到了这样的 Bug:

觉得还不够离谱?那么用手按住,哇,这真是太酷炫了!

Screenshot 2026-07-24 at 15.47.53

解决方法也很简单,dismiss 的动画播放完成了再替换就行了。FB22597916(在 iOS 27 已经确认修复)

TabBar 影响别人的问题

如果你的 App 中有 WKWebView,那你要提防这个问题。即便你的网页在 Safari 中显示正常,在 WKWebView 中也不一定。在有 TabBar 的环境下,WKWebView 中的网页内容会自动延伸到 TabBar 底部。这对于一般的网页内容无关紧要,因为当你滚动到最下方的时候会被自动加上一段空白,不会挡住,但定义为 position: fixed; 的元素就不是这么回事了。

解决的方法也很简单,在网页的 META 中加入 viewport-fit=cover 的声明,同时在计算 CSS 的时候使用 env(safe-area-inset-bottom)env(safe-area-inset-top) 即可得到正确的布局。

需要注意的是,如果你的网页不知道因为什么原因,会有多个 name="viewport" 的 META,你每一个里面都要加。

无解的问题。

除了上面这些之外,还有一些根本无解的问题。如果你用了 Stepper,并且同时使用了会弹出键盘的 UI 元素,你就可能会遇到 Stepper 位置向上漂移的问题。漂移前后效果如图:

这个问题与你是否将 Stepper 之后移动到键盘上方不被遮挡的地方无关,这么点漂移也肯定不是为了让 Stepper 不被遮挡(你看这图上不是还挡着呢么)。只要你的 Stepper 初始位置位于可以被键盘挡住的区域,就可能会遇到这个问题。FB20865249(从 iOS 26 第一个版本就有,知道 27 依然没人管)

一定要每个版本都测过一遍

现在同样的 UI 在 iOS 18,iOS 26,以及将来的 iOS 27 都可能会出现完全不一样的行为。不同的版本有不同的“特性”,不同的使用方式也会带来不一样的问题,建议大家手里准备好每个版本的设备,尤其对 Navigation 相关的部分做好测试,避免被坑。还有就是,尽量减少 App 中与苹果推荐的标准做法不同的设计,差的越多越可能落到苹果没有测试到的范围,越可能遇到莫名其妙的问题。

差不多也就是这个样子了,我想大家看完也知道为什么这篇文章我拖这么久了。累了,散了吧散了吧。

About Author

Megabits 金鱼一条。独立开发十年有余,实为兴趣使然,鲜有成就。现居日本,设计专业毕业,继续写代码糊口。想当个艺术家,但想想还是算了^_^。

订阅 Fatbobman 周报

每周精选 Swift 与 SwiftUI 开发技巧,加入众多开发者的行列。

立即订阅