尽管 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。下面的代码我会做很多省略,只留下重要的部分。
// 传入尺寸
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 时传入的尺寸被系统给动过了,不是正方形了。加两行试试看呢?
widthAnchor.constraint(equalToConstant: imageFrame.width),
heightAnchor.constraint(equalToConstant: imageFrame.height),
似乎没什么效果。
原来,还要让图片完全不依赖外部宽高布局才行。
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 画到外面,这个按钮就基本上完美了。
item.badge = .string("3")
item.badge?.backgroundColor = .systemRed
item.badge?.foregroundColor = .white
还没完呢
你以为这就完了?一切才刚刚开始。iOS 26 开始提供的这一套角标 API 有一个非常致命的问题,就是内容可能会不更新。具体什么时候会发生我已经懒得追究了,总之最简单的解决办法就是在改了角标之后把 UIBarButtonItem 的内容动一下,比如像下面这样,拿下去再拿上来,立竿见影。
item.badge = .string("3")
item.customView = nil
item.customView = customView
还有一个问题是,如果你按钮是用 rightBarButtonItems 设置的,你就可能会遇到按钮顺序反过来的情况,这似乎是和加载的时间点有关的,因为 DispatchQueue.main.async 一下我这里就能解决,不过还是推荐大家用新的 trailingItemGroups。
self.navigationItem.trailingItemGroups = [
UIBarButtonItemGroup(barButtonItems: [rightBarButtonItems1], representativeItem: nil),
UIBarButtonItemGroup(barButtonItems: [rightBarButtonItems2], representativeItem: nil)
]
不跟上 HIG 的代价
目前我手上这个 App 会在 NavigationBar 上设置背景色。这个行为现在已经是不推荐的了。如果你非要设置,就会出现一个坑就是 UIBarButtonItem 并不是根据 NavigationBar 的背景色,而是根据更底层 ScrollView 中的内容变深浅模式的。但是!颜色却是跟着这个背景色的。这就导致浅色的时候看起来还好:
深色的时候就非常恐怖:
我找到的看起来效果还行的方案就是给整个按钮固定成白色。
scanItem.style = .prominent
scanItem.tintColor = .white
此外在这种情况下还有一个很细节的问题,如果你的图标不是黑色的,你用 UIKit 的 CustomView 和 SwiftUI 的 CustomView 会渲染出不一样的颜色。UIKit 的 CustomView 看起来要黑一些,受底部背景色的影响,和不用 CustomView 的时候效果是一致的。SwiftUI 的则并不受底部背景色的影响,接近原本的颜色。
UITabBarController
TabBar 自己的问题
我们来看下面这样一个美丽的 TabBar。
假设你的 App 中有一个用于登陆的 sheet,在用户登录后,你需要重写 TabBar 的内容。于是你在用户登录之后删除了并重新放置了 UITabBarController。
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:
觉得还不够离谱?那么用手按住,哇,这真是太酷炫了!
解决方法也很简单,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 中与苹果推荐的标准做法不同的设计,差的越多越可能落到苹果没有测试到的范围,越可能遇到莫名其妙的问题。
差不多也就是这个样子了,我想大家看完也知道为什么这篇文章我拖这么久了。累了,散了吧散了吧。